From 6e26af105e32591ddb506c61553a62e9f0fbc561 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 3 May 2021 10:55:47 +0300 Subject: [PATCH 01/36] [Discover] migrate remaining context files from js to ts --- .../context/api/{_stubs.js => _stubs.ts} | 61 +++++----- .../api/{anchor.test.js => anchor.test.ts} | 70 +++++++----- .../context/api/{anchor.js => anchor.ts} | 25 +++- ...s.test.js => context.predecessors.test.ts} | 107 ++++++++++-------- ...ors.test.js => context.successors.test.ts} | 90 ++++++++------- .../angular/context/api/context.ts | 7 +- .../context/query/{actions.js => actions.tsx} | 80 ++++++++----- .../context/query/{index.js => index.ts} | 0 .../context/query/{state.js => state.ts} | 10 +- .../context/query_parameters/actions.test.ts | 45 +++++--- .../{actions.js => actions.ts} | 36 ++++-- .../query_parameters/{index.js => index.ts} | 0 .../application/angular/context_app_state.ts | 60 ++++++++++ 13 files changed, 368 insertions(+), 223 deletions(-) rename src/plugins/discover/public/application/angular/context/api/{_stubs.js => _stubs.ts} (58%) rename src/plugins/discover/public/application/angular/context/api/{anchor.test.js => anchor.test.ts} (75%) rename src/plugins/discover/public/application/angular/context/api/{anchor.js => anchor.ts} (65%) rename src/plugins/discover/public/application/angular/context/api/{context.predecessors.test.js => context.predecessors.test.ts} (80%) rename src/plugins/discover/public/application/angular/context/api/{context.successors.test.js => context.successors.test.ts} (83%) rename src/plugins/discover/public/application/angular/context/query/{actions.js => actions.tsx} (66%) rename src/plugins/discover/public/application/angular/context/query/{index.js => index.ts} (100%) rename src/plugins/discover/public/application/angular/context/query/{state.js => state.ts} (57%) rename src/plugins/discover/public/application/angular/context/query_parameters/{actions.js => actions.ts} (51%) rename src/plugins/discover/public/application/angular/context/query_parameters/{index.js => index.ts} (100%) create mode 100644 src/plugins/discover/public/application/angular/context_app_state.ts diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.js b/src/plugins/discover/public/application/angular/context/api/_stubs.ts similarity index 58% rename from src/plugins/discover/public/application/angular/context/api/_stubs.js rename to src/plugins/discover/public/application/angular/context/api/_stubs.ts index 6930e96a0d411..91cf34c44efcd 100644 --- a/src/plugins/discover/public/application/angular/context/api/_stubs.js +++ b/src/plugins/discover/public/application/angular/context/api/_stubs.ts @@ -9,8 +9,11 @@ import sinon from 'sinon'; import moment from 'moment'; +import { IndexPatternsContract } from '../../../../../../data/public'; +import { EsHitRecord, EsHitRecordList } from './context'; + export function createIndexPatternsStub() { - return { + return ({ get: sinon.spy((indexPatternId) => Promise.resolve({ id: indexPatternId, @@ -18,62 +21,58 @@ export function createIndexPatternsStub() { popularizeField: () => {}, }) ), - }; + } as unknown) as IndexPatternsContract; } /** * A stubbed search source with a `fetch` method that returns all of `_stubHits`. */ -export function createSearchSourceStub(hits, timeField) { - const searchSourceStub = { +export function createSearchSourceStub(hits: Array>, timeField?: string) { + const searchSourceStub: any = { _stubHits: hits, _stubTimeField: timeField, - _createStubHit: (timestamp, tiebreaker = 0) => ({ + _createStubHit: (timestamp: number, tiebreaker = 0) => ({ [searchSourceStub._stubTimeField]: timestamp, sort: [timestamp, tiebreaker], }), + setParent: sinon.spy(() => searchSourceStub), + setField: sinon.spy(() => searchSourceStub), + removeField: sinon.spy(() => searchSourceStub), + getField: sinon.spy((key) => { + const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; + return previousSetCall ? previousSetCall.args[1] : null; + }), + fetch: sinon.spy(() => + Promise.resolve({ + hits: { + hits: searchSourceStub._stubHits, + total: searchSourceStub._stubHits.length, + }, + }) + ), }; - - searchSourceStub.setParent = sinon.spy(() => searchSourceStub); - searchSourceStub.setField = sinon.spy(() => searchSourceStub); - searchSourceStub.removeField = sinon.spy(() => searchSourceStub); - - searchSourceStub.getField = sinon.spy((key) => { - const previousSetCall = searchSourceStub.setField.withArgs(key).lastCall; - return previousSetCall ? previousSetCall.args[1] : null; - }); - - searchSourceStub.fetch = sinon.spy(() => - Promise.resolve({ - hits: { - hits: searchSourceStub._stubHits, - total: searchSourceStub._stubHits.length, - }, - }) - ); - return searchSourceStub; } /** * A stubbed search source with a `fetch` method that returns a filtered set of `_stubHits`. */ -export function createContextSearchSourceStub(hits, timeField = '@timestamp') { - const searchSourceStub = createSearchSourceStub(hits, timeField); +export function createContextSearchSourceStub(hits: EsHitRecordList, timeFieldName = '@timestamp') { + const searchSourceStub = createSearchSourceStub(hits, timeFieldName); searchSourceStub.fetch = sinon.spy(() => { - const timeField = searchSourceStub._stubTimeField; + const timeField: keyof EsHitRecord = searchSourceStub._stubTimeField; const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1]; const timeRange = lastQuery.query.bool.must.constant_score.filter.range[timeField]; const lastSort = searchSourceStub.setField.withArgs('sort').lastCall.args[1]; const sortDirection = lastSort[0][timeField].order; const sortFunction = sortDirection === 'asc' - ? (first, second) => first[timeField] - second[timeField] - : (first, second) => second[timeField] - first[timeField]; + ? (first: any, second: any) => first[timeField] - second[timeField] + : (first: any, second: any) => second[timeField] - first[timeField]; const filteredHits = searchSourceStub._stubHits .filter( - (hit) => + (hit: EsHitRecord) => moment(hit[timeField]).isSameOrAfter(timeRange.gte) && moment(hit[timeField]).isSameOrBefore(timeRange.lte) ) @@ -87,5 +86,5 @@ export function createContextSearchSourceStub(hits, timeField = '@timestamp') { }); }); - return searchSourceStub; + return searchSourceStub as sinon.SinonStub; } diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.js b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts similarity index 75% rename from src/plugins/discover/public/application/angular/context/api/anchor.test.js rename to src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 12b9b4ab28556..4698dc8380e3c 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -6,24 +6,32 @@ * Side Public License, v 1. */ -import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; +import { Dictionary } from 'lodash'; -import { fetchAnchorProvider } from './anchor'; +import { SortDirection } from '../../../../../../data/public'; +import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; +import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; describe('context app', function () { - describe('function fetchAnchor', function () { - let fetchAnchor; - let searchSourceStub; + let fetchAnchor: ( + indexPatternId: string, + anchorId: string, + sort: [Dictionary, { [key: string]: SortDirection }] + ) => Promise; + let searchSourceStub: any; + describe('function fetchAnchor', function () { beforeEach(() => { - searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); + searchSourceStub = createSearchSourceStub([ + { _id: 'hit1', fields: [], sort: [], _source: {} }, + ]); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); it('should use the `fetch` method of the SearchSource', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { expect(searchSourceStub.fetch.calledOnce).toBe(true); }); @@ -31,8 +39,8 @@ describe('context app', function () { it('should configure the SearchSource to not inherit from the implicit root', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setParentSpy = searchSourceStub.setParent; expect(setParentSpy.calledOnce).toBe(true); @@ -42,8 +50,8 @@ describe('context app', function () { it('should set the SearchSource index pattern', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setFieldSpy = searchSourceStub.setField; expect(setFieldSpy.firstCall.args[1].id).toEqual('INDEX_PATTERN_ID'); @@ -52,8 +60,8 @@ describe('context app', function () { it('should set the SearchSource version flag to true', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setVersionSpy = searchSourceStub.setField.withArgs('version'); expect(setVersionSpy.calledOnce).toBe(true); @@ -63,8 +71,8 @@ describe('context app', function () { it('should set the SearchSource size to 1', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setSizeSpy = searchSourceStub.setField.withArgs('size'); expect(setSizeSpy.calledOnce).toBe(true); @@ -74,8 +82,8 @@ describe('context app', function () { it('should set the SearchSource query to an ids query', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setQuerySpy = searchSourceStub.setField.withArgs('query'); expect(setQuerySpy.calledOnce).toBe(true); @@ -96,12 +104,15 @@ describe('context app', function () { it('should set the SearchSource sort order', function () { return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setSortSpy = searchSourceStub.setField.withArgs('sort'); expect(setSortSpy.calledOnce).toBe(true); - expect(setSortSpy.firstCall.args[1]).toEqual([{ '@timestamp': 'desc' }, { _doc: 'desc' }]); + expect(setSortSpy.firstCall.args[1]).toEqual([ + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, + ]); }); }); @@ -109,11 +120,11 @@ describe('context app', function () { searchSourceStub._stubHits = []; return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then( () => { - expect().fail('expected the promise to be rejected'); + fail('expected the promise to be rejected'); }, (error) => { expect(error).toBeInstanceOf(Error); @@ -125,8 +136,8 @@ describe('context app', function () { searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then((anchorDocument) => { expect(anchorDocument).toHaveProperty('property1', 'value1'); expect(anchorDocument).toHaveProperty('$$_isAnchor', true); @@ -135,9 +146,6 @@ describe('context app', function () { }); describe('useNewFields API', () => { - let fetchAnchor; - let searchSourceStub; - beforeEach(() => { searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); @@ -147,8 +155,8 @@ describe('context app', function () { searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': 'desc' }, - { _doc: 'desc' }, + { '@timestamp': SortDirection.desc }, + { _doc: SortDirection.desc }, ]).then(() => { const setFieldsSpy = searchSourceStub.setField.withArgs('fields'); const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.js b/src/plugins/discover/public/application/angular/context/api/anchor.ts similarity index 65% rename from src/plugins/discover/public/application/angular/context/api/anchor.js rename to src/plugins/discover/public/application/angular/context/api/anchor.ts index 83b611cb0d648..e3e76a74f1688 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.js +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -6,11 +6,27 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import _, { Dictionary } from 'lodash'; import { i18n } from '@kbn/i18n'; -export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi = false) { - return async function fetchAnchor(indexPatternId, anchorId, sort) { +import { ISearchSource, IndexPatternsContract, SortDirection } from '../../../../../../data/public'; +import { EsHitRecord } from './context'; + +export interface AnchorHitRecord extends EsHitRecord { + // eslint-disable-next-line @typescript-eslint/naming-convention + $$_isAnchor: boolean; +} + +export function fetchAnchorProvider( + indexPatterns: IndexPatternsContract, + searchSource: ISearchSource, + useNewFieldsApi: boolean = false +) { + return async function fetchAnchor( + indexPatternId: string, + anchorId: string, + sort: [Dictionary, { [key: string]: SortDirection }] + ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) @@ -46,7 +62,8 @@ export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi return { ..._.get(response, ['hits', 'hits', 0]), + // eslint-disable-next-line @typescript-eslint/naming-convention $$_isAnchor: true, - }; + } as AnchorHitRecord; }; } diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts similarity index 80% rename from src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js rename to src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index 9f5e62da398d2..c75f09f8a9e5c 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -9,8 +9,10 @@ import moment from 'moment'; import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { fetchContextProvider } from './context'; -import { setServices } from '../../../../kibana_services'; +import { EsHitRecordList, fetchContextProvider } from './context'; +import { setServices, SortDirection } from '../../../../kibana_services'; +import { AnchorHitRecord } from './anchor'; +import { Query } from '../../../../../../data/public'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -18,13 +20,28 @@ const ANCHOR_TIMESTAMP_3 = new Date(MS_PER_DAY * 3).toJSON(); const ANCHOR_TIMESTAMP_1000 = new Date(MS_PER_DAY * 1000).toJSON(); const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); +interface Timestamp { + format: string; + gte?: string; + lte?: string; +} + describe('context app', function () { - describe('function fetchPredecessors', function () { - let fetchPredecessors; - let mockSearchSource; + let fetchPredecessors: ( + indexPatternId: string, + timeField: string, + sortDir: SortDirection, + timeValIso: string, + timeValNr: number, + tieBreakerField: string, + tieBreakerValue: number, + size: number + ) => Promise; + let mockSearchSource: any; + describe('function fetchPredecessors', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + mockSearchSource = createContextSearchSourceStub([], '@timestamp'); setServices({ data: { @@ -44,7 +61,7 @@ describe('context app', function () { timeValNr, tieBreakerField, tieBreakerValue, - size + size = 10 ) => { const anchor = { _source: { @@ -56,7 +73,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -78,14 +95,13 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 3, - [] - ).then((hits) => { + 3 + ).then((hits: EsHitRecordList) => { expect(mockSearchSource.fetch.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); }); @@ -103,17 +119,16 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 6, - [] - ).then((hits) => { - const intervals = mockSearchSource.setField.args - .filter(([property]) => property === 'query') - .map(([, { query }]) => + 6 + ).then((hits: EsHitRecordList) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: string) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) ); @@ -123,7 +138,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(Object.keys(last(intervals))).toEqual(['format', 'gte']); + expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'gte']); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); @@ -141,24 +156,23 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_1000, MS_PER_DAY * 1000, '_doc', 0, - 3, - [] - ).then((hits) => { - const intervals = mockSearchSource.setField.args - .filter(([property]) => property === 'query') - .map(([, { query }]) => - get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) - ); + 3 + ).then((hits: EsHitRecordList) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: string) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => { + return get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']); + }); // should have started at the given time expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 1000).toISOString()); // should have stopped before reaching MS_PER_DAY * 1700 - expect(moment(last(intervals).lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); + expect(moment(last(intervals)?.lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); }); @@ -168,14 +182,13 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, - 3, - [] - ).then((hits) => { + 3 + ).then((hits: EsHitRecordList) => { expect(hits).toEqual([]); }); }); @@ -184,13 +197,12 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, - 3, - [] + 3 ).then(() => { const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); @@ -202,13 +214,12 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP, MS_PER_DAY, '_doc', 0, - 3, - [] + 3 ).then(() => { expect( mockSearchSource.setField.calledWith('sort', [ @@ -221,11 +232,8 @@ describe('context app', function () { }); describe('function fetchPredecessors with useNewFieldsApi set', function () { - let fetchPredecessors; - let mockSearchSource; - beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + mockSearchSource = createContextSearchSourceStub([], '@timestamp'); setServices({ data: { @@ -245,7 +253,7 @@ describe('context app', function () { timeValNr, tieBreakerField, tieBreakerValue, - size + size = 10 ) => { const anchor = { _source: { @@ -257,7 +265,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -279,14 +287,13 @@ describe('context app', function () { return fetchPredecessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 3, - [] - ).then((hits) => { + 3 + ).then((hits: EsHitRecordList) => { const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); expect(mockSearchSource.fetch.calledOnce).toBe(true); diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts similarity index 83% rename from src/plugins/discover/public/application/angular/context/api/context.successors.test.js rename to src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index 4936c937aa2fa..3c2026b5282e5 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.js +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -10,20 +10,36 @@ import moment from 'moment'; import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { setServices } from '../../../../kibana_services'; - -import { fetchContextProvider } from './context'; +import { setServices, SortDirection } from '../../../../kibana_services'; +import { Query } from '../../../../../../data/public'; +import { EsHitRecordList, fetchContextProvider } from './context'; +import { AnchorHitRecord } from './anchor'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); const ANCHOR_TIMESTAMP_3 = new Date(MS_PER_DAY * 3).toJSON(); const ANCHOR_TIMESTAMP_3000 = new Date(MS_PER_DAY * 3000).toJSON(); +interface Timestamp { + format: string; + gte?: string; + lte?: string; +} + describe('context app', function () { - describe('function fetchSuccessors', function () { - let fetchSuccessors; - let mockSearchSource; + let fetchSuccessors: ( + indexPatternId: string, + timeField: string, + sortDir: SortDirection, + timeValIso: string, + timeValNr: number, + tieBreakerField: string, + tieBreakerValue: number, + size: number + ) => Promise; + let mockSearchSource: any; + describe('function fetchSuccessors', function () { beforeEach(() => { mockSearchSource = createContextSearchSourceStub([], '@timestamp'); @@ -57,7 +73,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'successors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -79,13 +95,12 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 3, - [] + 3 ).then((hits) => { expect(mockSearchSource.fetch.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); @@ -104,17 +119,16 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 6, - [] + 6 ).then((hits) => { - const intervals = mockSearchSource.setField.args - .filter(([property]) => property === 'query') - .map(([, { query }]) => + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: [string]) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) ); @@ -124,7 +138,7 @@ describe('context app', function () { // should have started at the given time expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have ended with a half-open interval - expect(Object.keys(last(intervals))).toEqual(['format', 'lte']); + expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'lte']); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); @@ -144,24 +158,23 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 4, - [] + 4 ).then((hits) => { - const intervals = mockSearchSource.setField.args - .filter(([property]) => property === 'query') - .map(([, { query }]) => + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: [string]) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) ); // should have started at the given time expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); // should have stopped before reaching MS_PER_DAY * 2200 - expect(moment(last(intervals).gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); + expect(moment(last(intervals)?.gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); expect(intervals.length).toBeGreaterThan(1); expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); @@ -172,13 +185,12 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, - 3, - [] + 3 ).then((hits) => { expect(hits).toEqual([]); }); @@ -188,13 +200,12 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, - 3, - [] + 3 ).then(() => { const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); @@ -206,18 +217,17 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP, MS_PER_DAY, '_doc', 0, - 3, - [] + 3 ).then(() => { expect( mockSearchSource.setField.calledWith('sort', [ - { '@timestamp': { order: 'desc', format: 'strict_date_optional_time' } }, - { _doc: 'desc' }, + { '@timestamp': { order: SortDirection.desc, format: 'strict_date_optional_time' } }, + { _doc: SortDirection.desc }, ]) ).toBe(true); }); @@ -225,9 +235,6 @@ describe('context app', function () { }); describe('function fetchSuccessors with useNewFieldsApi set', function () { - let fetchSuccessors; - let mockSearchSource; - beforeEach(() => { mockSearchSource = createContextSearchSourceStub([], '@timestamp'); @@ -261,7 +268,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'successors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -283,13 +290,12 @@ describe('context app', function () { return fetchSuccessors( 'INDEX_PATTERN_ID', '@timestamp', - 'desc', + SortDirection.desc, ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, - 3, - [] + 3 ).then((hits) => { expect(mockSearchSource.fetch.calledOnce).toBe(true); expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); 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 820e37d754ef2..e341c1e78dbe0 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -14,6 +14,7 @@ import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; +import { AnchorHitRecord } from './anchor'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -39,7 +40,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields * * @param {SurrDocType} type - `successors` or `predecessors` * @param {string} indexPatternId - * @param {EsHitRecord} anchor - anchor record + * @param {AnchorHitRecord} anchor - anchor record * @param {string} timeField - name of the timefield, that's sorted on * @param {string} tieBreakerField - name of the tie breaker, the 2nd sort field * @param {SortDirection} sortDir - direction of sorting @@ -50,13 +51,13 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields async function fetchSurroundingDocs( type: SurrDocType, indexPatternId: string, - anchor: EsHitRecord, + anchor: AnchorHitRecord, timeField: string, tieBreakerField: string, sortDir: SortDirection, size: number, filters: Filter[] - ) { + ): Promise { if (typeof anchor !== 'object' || anchor === null || !size) { return []; } diff --git a/src/plugins/discover/public/application/angular/context/query/actions.js b/src/plugins/discover/public/application/angular/context/query/actions.tsx similarity index 66% rename from src/plugins/discover/public/application/angular/context/query/actions.js rename to src/plugins/discover/public/application/angular/context/query/actions.tsx index 493697ff38ff1..6fadafe3a348b 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -8,17 +8,26 @@ import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -import React from 'react'; import { getServices } from '../../../../kibana_services'; -import { fetchAnchorProvider } from '../api/anchor'; -import { fetchContextProvider } from '../api/context'; +import { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor'; +import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; -import { FAILURE_REASONS, LOADING_STATUS } from './index'; -import { MarkdownSimple } from '../../../../../../kibana_react/public'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; +import { + ContextAppState, + FailureReason, + LoadingStatus, + LoadingStatusEntry, + LoadingStatusState, + QueryParameters, +} from '../../context_app_state'; + +interface DiscoverPromise extends PromiseConstructor { + try: (fn: () => Promise) => Promise; +} -export function QueryActionsProvider(Promise) { +export function QueryActionsProvider(Promise: DiscoverPromise) { const { filterManager, indexPatterns, data, uiSettings } = getServices(); const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const fetchAnchor = fetchAnchorProvider( @@ -32,24 +41,27 @@ export function QueryActionsProvider(Promise) { indexPatterns ); - const setFailedStatus = (state) => (subject, details = {}) => + const setFailedStatus = (state: ContextAppState) => ( + subject: keyof LoadingStatusState, + details: LoadingStatusEntry = {} + ) => (state.loadingStatus[subject] = { - status: LOADING_STATUS.FAILED, - reason: FAILURE_REASONS.UNKNOWN, + status: LoadingStatus.FAILED, + reason: FailureReason.UNKNOWN, ...details, }); - const setLoadedStatus = (state) => (subject) => + const setLoadedStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) => (state.loadingStatus[subject] = { - status: LOADING_STATUS.LOADED, + status: LoadingStatus.LOADED, }); - const setLoadingStatus = (state) => (subject) => + const setLoadingStatus = (state: ContextAppState) => (subject: keyof LoadingStatusState) => (state.loadingStatus[subject] = { - status: LOADING_STATUS.LOADING, + status: LoadingStatus.LOADING, }); - const fetchAnchorRow = (state) => () => { + const fetchAnchorRow = (state: ContextAppState) => () => { const { queryParameters: { indexPatternId, anchorId, sort, tieBreakerField }, } = state; @@ -57,7 +69,7 @@ export function QueryActionsProvider(Promise) { if (!tieBreakerField) { return Promise.reject( setFailedStatus(state)('anchor', { - reason: FAILURE_REASONS.INVALID_TIEBREAKER, + reason: FailureReason.INVALID_TIEBREAKER, }) ); } @@ -67,25 +79,25 @@ export function QueryActionsProvider(Promise) { return Promise.try(() => fetchAnchor(indexPatternId, anchorId, [_.fromPairs([sort]), { [tieBreakerField]: sort[1] }]) ).then( - (anchorDocument) => { + (anchorDocument: AnchorHitRecord) => { setLoadedStatus(state)('anchor'); state.rows.anchor = anchorDocument; return anchorDocument; }, - (error) => { + (error: Error) => { setFailedStatus(state)('anchor', { error }); getServices().toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', }), - text: {error.message}, + text: error.message, }); throw error; } ); }; - const fetchSurroundingRows = (type, state) => { + const fetchSurroundingRows = (type: SurrDocType, state: ContextAppState) => { const { queryParameters: { indexPatternId, sort, tieBreakerField }, rows: { anchor }, @@ -100,7 +112,7 @@ export function QueryActionsProvider(Promise) { if (!tieBreakerField) { return Promise.reject( setFailedStatus(state)(type, { - reason: FAILURE_REASONS.INVALID_TIEBREAKER, + reason: FailureReason.INVALID_TIEBREAKER, }) ); } @@ -120,54 +132,62 @@ export function QueryActionsProvider(Promise) { filters ) ).then( - (documents) => { + (documents: EsHitRecordList) => { setLoadedStatus(state)(type); state.rows[type] = documents; return documents; }, - (error) => { + (error: Error) => { setFailedStatus(state)(type, { error }); getServices().toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', }), - text: {error.message}, + text: error.message, }); throw error; } ); }; - const fetchContextRows = (state) => () => + const fetchContextRows = (state: ContextAppState) => () => Promise.all([ fetchSurroundingRows('predecessors', state), fetchSurroundingRows('successors', state), ]); - const fetchAllRows = (state) => () => + const fetchAllRows = (state: ContextAppState) => () => Promise.try(fetchAnchorRow(state)).then(fetchContextRows(state)); - const fetchContextRowsWithNewQueryParameters = (state) => (queryParameters) => { + const fetchContextRowsWithNewQueryParameters = (state: ContextAppState) => ( + queryParameters: QueryParameters + ) => { setQueryParameters(state)(queryParameters); return fetchContextRows(state)(); }; - const fetchAllRowsWithNewQueryParameters = (state) => (queryParameters) => { + const fetchAllRowsWithNewQueryParameters = (state: ContextAppState) => ( + queryParameters: QueryParameters + ) => { setQueryParameters(state)(queryParameters); return fetchAllRows(state)(); }; - const fetchGivenPredecessorRows = (state) => (count) => { + const fetchGivenPredecessorRows = (state: ContextAppState) => (count: number) => { setPredecessorCount(state)(count); return fetchSurroundingRows('predecessors', state); }; - const fetchGivenSuccessorRows = (state) => (count) => { + const fetchGivenSuccessorRows = (state: ContextAppState) => (count: number) => { setSuccessorCount(state)(count); return fetchSurroundingRows('successors', state); }; - const setAllRows = (state) => (predecessorRows, anchorRow, successorRows) => + const setAllRows = (state: ContextAppState) => ( + predecessorRows: EsHitRecordList, + anchorRow: EsHitRecord, + successorRows: EsHitRecordList + ) => (state.rows.all = [ ...(predecessorRows || []), ...(anchorRow ? [anchorRow] : []), diff --git a/src/plugins/discover/public/application/angular/context/query/index.js b/src/plugins/discover/public/application/angular/context/query/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query/index.js rename to src/plugins/discover/public/application/angular/context/query/index.ts diff --git a/src/plugins/discover/public/application/angular/context/query/state.js b/src/plugins/discover/public/application/angular/context/query/state.ts similarity index 57% rename from src/plugins/discover/public/application/angular/context/query/state.js rename to src/plugins/discover/public/application/angular/context/query/state.ts index c4f3fbb80cce4..fefadf9009185 100644 --- a/src/plugins/discover/public/application/angular/context/query/state.js +++ b/src/plugins/discover/public/application/angular/context/query/state.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import { LOADING_STATUS } from './index'; +import { LoadingStatus, LoadingStatusState } from '../../context_app_state'; -export function createInitialLoadingStatusState() { +export function createInitialLoadingStatusState(): LoadingStatusState { return { - anchor: LOADING_STATUS.UNINITIALIZED, - predecessors: LOADING_STATUS.UNINITIALIZED, - successors: LOADING_STATUS.UNINITIALIZED, + anchor: LoadingStatus.UNINITIALIZED, + predecessors: LoadingStatus.UNINITIALIZED, + successors: LoadingStatus.UNINITIALIZED, }; } 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 774a29721c426..fe00d11f0d3b6 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 @@ -8,18 +8,12 @@ // @ts-ignore import { getQueryParameterActions } from './actions'; -import { FilterManager } from '../../../../../../data/public'; +import { FilterManager, SortDirection } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; +import { ContextAppState, LoadingStatus, QueryParameters } from '../../context_app_state'; const setupMock = coreMock.createSetup(); -let state: { - queryParameters: { - defaultStepSize: number; - indexPatternId: string; - predecessorCount: number; - successorCount: number; - }; -}; +let state: ContextAppState; let filterManager: FilterManager; let filterManagerSpy: jest.SpyInstance; @@ -33,7 +27,25 @@ beforeEach(() => { indexPatternId: 'INDEX_PATTERN_ID', predecessorCount: 10, successorCount: 10, + anchorId: '', + columns: [], + filters: [], + sort: ['field', SortDirection.asc], + tieBreakerField: '', + }, + loadingStatus: { + anchor: LoadingStatus.UNINITIALIZED, + predecessors: LoadingStatus.UNINITIALIZED, + successors: LoadingStatus.UNINITIALIZED, }, + rows: { + all: [], + // eslint-disable-next-line @typescript-eslint/naming-convention + anchor: { $$_isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, + predecessors: [], + successors: [], + }, + useNewFieldsApi: true, }; }); @@ -105,6 +117,7 @@ describe('context query_parameter actions', function () { const newState = { ...state, queryParameters: { + ...state.queryParameters, additionalParameter: 'ADDITIONAL_PARAMETER', }, }; @@ -113,11 +126,12 @@ describe('context query_parameter actions', function () { anchorId: 'ANCHOR_ID', columns: ['column'], defaultStepSize: 3, - filters: ['filter'], + filters: [], indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field'], + sort: ['field', SortDirection.asc], + tieBreakerField: '', }); expect(actualState).toEqual({ @@ -125,20 +139,21 @@ describe('context query_parameter actions', function () { anchorId: 'ANCHOR_ID', columns: ['column'], defaultStepSize: 3, - filters: ['filter'], + filters: [], indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field'], + sort: ['field', SortDirection.asc], + tieBreakerField: '', }); }); it('should ignore invalid properties', function () { const newState = { ...state }; - setQueryParameters(newState)({ + setQueryParameters(newState)(({ additionalParameter: 'ADDITIONAL_PARAMETER', - }); + } as unknown) as QueryParameters); expect(state.queryParameters).toEqual(newState.queryParameters); }); diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.js b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts similarity index 51% rename from src/plugins/discover/public/application/angular/context/query_parameters/actions.js rename to src/plugins/discover/public/application/angular/context/query_parameters/actions.ts index b308196cb8b04..d7507a35e0996 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.js +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts @@ -7,34 +7,46 @@ */ import _ from 'lodash'; -import { esFilters } from '../../../../../../data/public'; +import { esFilters, Filter, IFieldType } from '../../../../../../data/public'; +import { FilterManager, IndexPatternsContract } from '../../../../../../data/public'; import { popularizeField } from '../../../helpers/popularize_field'; +import { ContextAppState, QueryParameters } from '../../context_app_state'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; -export function getQueryParameterActions(filterManager, indexPatterns) { - const setPredecessorCount = (state) => (predecessorCount) => - (state.queryParameters.predecessorCount = clamp( +export function getQueryParameterActions( + filterManager: FilterManager, + indexPatterns?: IndexPatternsContract +) { + const setPredecessorCount = (state: ContextAppState) => (predecessorCount: number) => { + return (state.queryParameters.predecessorCount = clamp( MIN_CONTEXT_SIZE, MAX_CONTEXT_SIZE, predecessorCount )); + }; - const setSuccessorCount = (state) => (successorCount) => - (state.queryParameters.successorCount = clamp( + const setSuccessorCount = (state: ContextAppState) => (successorCount: number) => { + return (state.queryParameters.successorCount = clamp( MIN_CONTEXT_SIZE, MAX_CONTEXT_SIZE, successorCount )); + }; - const setQueryParameters = (state) => (queryParameters) => - Object.assign(state.queryParameters, _.pick(queryParameters, QUERY_PARAMETER_KEYS)); + const setQueryParameters = (state: ContextAppState) => (queryParameters: QueryParameters) => { + return Object.assign(state.queryParameters, _.pick(queryParameters, QUERY_PARAMETER_KEYS)); + }; - const updateFilters = () => (filters) => { + const updateFilters = () => (filters: Filter[]) => { filterManager.setFilters(filters); }; - const addFilter = (state) => async (field, values, operation) => { + const addFilter = (state: ContextAppState) => async ( + field: string | IFieldType, + values: any, + operation: string + ) => { const indexPatternId = state.queryParameters.indexPatternId; const newFilters = esFilters.generateFilters( filterManager, @@ -46,7 +58,7 @@ export function getQueryParameterActions(filterManager, indexPatterns) { filterManager.addFilters(newFilters); if (indexPatterns) { const indexPattern = await indexPatterns.get(indexPatternId); - await popularizeField(indexPattern, field.name, indexPatterns); + await popularizeField(indexPattern, (field as IFieldType).name, indexPatterns); } }; @@ -59,6 +71,6 @@ export function getQueryParameterActions(filterManager, indexPatterns) { }; } -function clamp(minimum, maximum, value) { +function clamp(minimum: number, maximum: number, value: number) { return Math.max(Math.min(maximum, value), minimum); } diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/index.js b/src/plugins/discover/public/application/angular/context/query_parameters/index.ts similarity index 100% rename from src/plugins/discover/public/application/angular/context/query_parameters/index.js rename to src/plugins/discover/public/application/angular/context/query_parameters/index.ts diff --git a/src/plugins/discover/public/application/angular/context_app_state.ts b/src/plugins/discover/public/application/angular/context_app_state.ts new file mode 100644 index 0000000000000..1593b2457019c --- /dev/null +++ b/src/plugins/discover/public/application/angular/context_app_state.ts @@ -0,0 +1,60 @@ +/* + * 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 { Filter } from '../../../../data/public'; +import { AnchorHitRecord } from './context/api/anchor'; +import { EsHitRecordList } from './context/api/context'; +import { SortDirection } from './context/api/utils/sorting'; + +export interface ContextAppState { + loadingStatus: LoadingStatusState; + queryParameters: QueryParameters; + rows: ContextRows; + useNewFieldsApi: boolean; +} + +export enum LoadingStatus { + FAILED = 'failed', + LOADED = 'loaded', + LOADING = 'loading', + UNINITIALIZED = 'uninitialized', +} +export enum FailureReason { + UNKNOWN = 'unknown', + INVALID_TIEBREAKER = 'invalid_tiebreaker', +} +export type LoadingStatusEntry = Partial<{ + status: LoadingStatus; + reason: FailureReason; + error: Error; +}>; + +export interface LoadingStatusState { + anchor: LoadingStatusEntry | LoadingStatus; + predecessors: LoadingStatusEntry | LoadingStatus; + successors: LoadingStatusEntry | LoadingStatus; +} + +export interface QueryParameters { + anchorId: string; + columns: string[]; + defaultStepSize: number; + filters: Filter[]; + indexPatternId: string; + predecessorCount: number; + successorCount: number; + sort: [string, SortDirection]; + tieBreakerField: string; +} + +interface ContextRows { + all: EsHitRecordList; + anchor: AnchorHitRecord; + predecessors: EsHitRecordList; + successors: EsHitRecordList; +} From fae2360d35339c393a65502b3c49eaf520b03252 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 3 May 2021 18:34:41 +0300 Subject: [PATCH 02/36] [Discover] get rid of any types --- .../application/angular/context/api/_stubs.ts | 22 ++++++++++++------- .../angular/context/api/anchor.test.ts | 4 +++- .../context/api/context.predecessors.test.ts | 4 ++-- .../context/api/context.successors.test.ts | 4 ++-- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.ts b/src/plugins/discover/public/application/angular/context/api/_stubs.ts index 91cf34c44efcd..3736065492e06 100644 --- a/src/plugins/discover/public/application/angular/context/api/_stubs.ts +++ b/src/plugins/discover/public/application/angular/context/api/_stubs.ts @@ -10,7 +10,13 @@ import sinon from 'sinon'; import moment from 'moment'; import { IndexPatternsContract } from '../../../../../../data/public'; -import { EsHitRecord, EsHitRecordList } from './context'; +import { EsHitRecordList } from './context'; + +type SortHit = { + [key in string]: number; // timeField name +} & { + sort: [number, number]; +}; export function createIndexPatternsStub() { return ({ @@ -27,7 +33,7 @@ export function createIndexPatternsStub() { /** * A stubbed search source with a `fetch` method that returns all of `_stubHits`. */ -export function createSearchSourceStub(hits: Array>, timeField?: string) { +export function createSearchSourceStub(hits: EsHitRecordList, timeField?: string) { const searchSourceStub: any = { _stubHits: hits, _stubTimeField: timeField, @@ -57,22 +63,22 @@ export function createSearchSourceStub(hits: Array>, timeFi /** * A stubbed search source with a `fetch` method that returns a filtered set of `_stubHits`. */ -export function createContextSearchSourceStub(hits: EsHitRecordList, timeFieldName = '@timestamp') { - const searchSourceStub = createSearchSourceStub(hits, timeFieldName); +export function createContextSearchSourceStub(timeFieldName: string) { + const searchSourceStub = createSearchSourceStub([], timeFieldName); searchSourceStub.fetch = sinon.spy(() => { - const timeField: keyof EsHitRecord = searchSourceStub._stubTimeField; + const timeField: keyof SortHit = searchSourceStub._stubTimeField; const lastQuery = searchSourceStub.setField.withArgs('query').lastCall.args[1]; const timeRange = lastQuery.query.bool.must.constant_score.filter.range[timeField]; const lastSort = searchSourceStub.setField.withArgs('sort').lastCall.args[1]; const sortDirection = lastSort[0][timeField].order; const sortFunction = sortDirection === 'asc' - ? (first: any, second: any) => first[timeField] - second[timeField] - : (first: any, second: any) => second[timeField] - first[timeField]; + ? (first: SortHit, second: SortHit) => first[timeField] - second[timeField] + : (first: SortHit, second: SortHit) => second[timeField] - first[timeField]; const filteredHits = searchSourceStub._stubHits .filter( - (hit: EsHitRecord) => + (hit: SortHit) => moment(hit[timeField]).isSameOrAfter(timeRange.gte) && moment(hit[timeField]).isSameOrBefore(timeRange.lte) ) diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 4698dc8380e3c..080a3066c7be5 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -147,7 +147,9 @@ describe('context app', function () { describe('useNewFields API', () => { beforeEach(() => { - searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); + searchSourceStub = createSearchSourceStub([ + { _id: 'hit1', fields: [], sort: [], _source: {} }, + ]); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index c75f09f8a9e5c..ec567b40f8eb0 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -41,7 +41,7 @@ describe('context app', function () { describe('function fetchPredecessors', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); setServices({ data: { @@ -233,7 +233,7 @@ describe('context app', function () { describe('function fetchPredecessors with useNewFieldsApi set', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); setServices({ data: { diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index 3c2026b5282e5..987c56514b77c 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -41,7 +41,7 @@ describe('context app', function () { describe('function fetchSuccessors', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); setServices({ data: { @@ -236,7 +236,7 @@ describe('context app', function () { describe('function fetchSuccessors with useNewFieldsApi set', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); setServices({ data: { From 5c9b64ab375cd564542d39a76ae78f7dfaace6c5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 4 May 2021 11:58:37 +0300 Subject: [PATCH 03/36] [Discover] replace constants with enums, update imports --- .../angular/context/api/anchor.test.ts | 6 ++---- .../application/angular/context/api/anchor.ts | 14 +++++++++----- .../angular/context/query/actions.tsx | 14 ++++++++------ .../angular/context/query/index.ts | 1 - .../context/query_parameters/actions.test.ts | 1 - .../context/query_parameters/actions.ts | 6 +++--- .../public/application/angular/context_app.js | 12 +----------- .../application/angular/context_state.ts | 4 ++-- .../components/context_app/constants.ts | 19 ------------------- .../context_app/context_app_legacy.tsx | 10 +++++----- .../context_error_message.test.tsx | 11 +++++------ .../context_error_message.tsx | 7 +++---- 12 files changed, 38 insertions(+), 67 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/context_app/constants.ts diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 080a3066c7be5..5f63ac1592a7c 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -6,9 +6,7 @@ * Side Public License, v 1. */ -import { Dictionary } from 'lodash'; - -import { SortDirection } from '../../../../../../data/public'; +import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; @@ -16,7 +14,7 @@ describe('context app', function () { let fetchAnchor: ( indexPatternId: string, anchorId: string, - sort: [Dictionary, { [key: string]: SortDirection }] + sort: EsQuerySortValue[] ) => Promise; let searchSourceStub: any; diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/angular/context/api/anchor.ts index e3e76a74f1688..da81ce525331a 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -6,10 +6,14 @@ * Side Public License, v 1. */ -import _, { Dictionary } from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { ISearchSource, IndexPatternsContract, SortDirection } from '../../../../../../data/public'; +import { + ISearchSource, + IndexPatternsContract, + EsQuerySortValue, +} from '../../../../../../data/public'; import { EsHitRecord } from './context'; export interface AnchorHitRecord extends EsHitRecord { @@ -25,7 +29,7 @@ export function fetchAnchorProvider( return async function fetchAnchor( indexPatternId: string, anchorId: string, - sort: [Dictionary, { [key: string]: SortDirection }] + sort: EsQuerySortValue[] ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource @@ -52,7 +56,7 @@ export function fetchAnchorProvider( } const response = await searchSource.fetch(); - if (_.get(response, ['hits', 'total'], 0) < 1) { + if (get(response, ['hits', 'total'], 0) < 1) { throw new Error( i18n.translate('discover.context.failedToLoadAnchorDocumentErrorDescription', { defaultMessage: 'Failed to load anchor document.', @@ -61,7 +65,7 @@ export function fetchAnchorProvider( } return { - ..._.get(response, ['hits', 'hits', 0]), + ...get(response, ['hits', 'hits', 0]), // eslint-disable-next-line @typescript-eslint/naming-convention $$_isAnchor: true, } as AnchorHitRecord; 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 6fadafe3a348b..52c56d379d259 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -6,14 +6,16 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import React from 'react'; +import { fromPairs } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { getServices } from '../../../../kibana_services'; +import { getServices } from '../../../../kibana_services'; +import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; +import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; import { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor'; import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; import { ContextAppState, FailureReason, @@ -77,7 +79,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { setLoadingStatus(state)('anchor'); return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [_.fromPairs([sort]), { [tieBreakerField]: sort[1] }]) + fetchAnchor(indexPatternId, anchorId, [fromPairs([sort]), { [tieBreakerField]: sort[1] }]) ).then( (anchorDocument: AnchorHitRecord) => { setLoadedStatus(state)('anchor'); @@ -90,7 +92,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', }), - text: error.message, + text: toMountPoint({error.message}), }); throw error; } @@ -143,7 +145,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { title: i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', }), - text: error.message, + text: toMountPoint({error.message}), }); throw error; } diff --git a/src/plugins/discover/public/application/angular/context/query/index.ts b/src/plugins/discover/public/application/angular/context/query/index.ts index a718cdb237774..70e3dc1600472 100644 --- a/src/plugins/discover/public/application/angular/context/query/index.ts +++ b/src/plugins/discover/public/application/angular/context/query/index.ts @@ -7,5 +7,4 @@ */ export { QueryActionsProvider } from './actions'; -export { FAILURE_REASONS, LOADING_STATUS } from '../../../components/context_app/constants'; export { createInitialLoadingStatusState } from './state'; 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 fe00d11f0d3b6..b54f11e9e6706 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 @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -// @ts-ignore import { getQueryParameterActions } from './actions'; import { FilterManager, SortDirection } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; 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 d7507a35e0996..03372c4fa2ccf 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 @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { pick } from 'lodash'; + import { esFilters, Filter, IFieldType } from '../../../../../../data/public'; import { FilterManager, IndexPatternsContract } from '../../../../../../data/public'; import { popularizeField } from '../../../helpers/popularize_field'; import { ContextAppState, QueryParameters } from '../../context_app_state'; - import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; export function getQueryParameterActions( @@ -35,7 +35,7 @@ export function getQueryParameterActions( }; const setQueryParameters = (state: ContextAppState) => (queryParameters: QueryParameters) => { - return Object.assign(state.queryParameters, _.pick(queryParameters, QUERY_PARAMETER_KEYS)); + return Object.assign(state.queryParameters, pick(queryParameters, QUERY_PARAMETER_KEYS)); }; const updateFilters = () => (filters: Filter[]) => { diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index 04406338f49ab..a90904fa2ccea 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -21,12 +21,7 @@ import { getQueryParameterActions, QUERY_PARAMETER_KEYS, } from './context/query_parameters'; -import { - createInitialLoadingStatusState, - FAILURE_REASONS, - LOADING_STATUS, - QueryActionsProvider, -} from './context/query'; +import { createInitialLoadingStatusState, QueryActionsProvider } from './context/query'; import { callAfterBindingsWorkaround } from './context/helpers/call_after_bindings_workaround'; getAngularModule().directive('contextApp', function ContextApp() { @@ -69,11 +64,6 @@ function ContextAppController($scope, Private) { (action) => (...args) => action(this.state)(...args) ); - this.constants = { - FAILURE_REASONS, - LOADING_STATUS, - }; - $scope.$watchGroup( [ () => this.state.rows.predecessors, diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 0bae006ec1f6e..4a9a729e9229b 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { isEqual } from 'lodash'; import { History } from 'history'; import { NotificationsStart, IUiSettingsClient } from 'kibana/public'; import { @@ -247,7 +247,7 @@ function isEqualState(stateA: AppState | GlobalState, stateB: AppState | GlobalS const { filters: stateAFilters = [], ...stateAPartial } = stateA; const { filters: stateBFilters = [], ...stateBPartial } = stateB; return ( - _.isEqual(stateAPartial, stateBPartial) && + isEqual(stateAPartial, stateBPartial) && esFilters.compareFilters(stateAFilters, stateBFilters, esFilters.COMPARE_ALL_OPTIONS) ); } diff --git a/src/plugins/discover/public/application/components/context_app/constants.ts b/src/plugins/discover/public/application/components/context_app/constants.ts deleted file mode 100644 index a22aa69477ee9..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/constants.ts +++ /dev/null @@ -1,19 +0,0 @@ -/* - * 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. - */ - -export const FAILURE_REASONS = { - UNKNOWN: 'unknown', - INVALID_TIEBREAKER: 'invalid_tiebreaker', -}; - -export const LOADING_STATUS = { - FAILED: 'failed', - LOADED: 'loaded', - LOADING: 'loading', - UNINITIALIZED: 'uninitialized', -}; 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 5031f78c49fcc..55c2208105f13 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 @@ -15,7 +15,7 @@ import { DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; -import { LOADING_STATUS } from './constants'; +import { LoadingStatus } from '../../angular/context_app_state'; import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; import { TopNavMenuProps } from '../../../../../navigation/public'; @@ -45,13 +45,13 @@ const PREDECESSOR_TYPE = 'predecessors'; const SUCCESSOR_TYPE = 'successors'; function isLoading(status: string) { - return status !== LOADING_STATUS.LOADED && status !== LOADING_STATUS.FAILED; + return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; } export function ContextAppLegacy(renderProps: ContextAppProps) { const status = renderProps.status; - const isLoaded = status === LOADING_STATUS.LOADED; - const isFailed = status === LOADING_STATUS.FAILED; + const isLoaded = status === LoadingStatus.LOADED; + const isFailed = status === LoadingStatus.FAILED; const actionBarProps = (type: string) => { const { @@ -114,7 +114,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const loadingFeedback = () => { - if (status === LOADING_STATUS.UNINITIALIZED || status === LOADING_STATUS.LOADING) { + if (status === LoadingStatus.UNINITIALIZED || status === LoadingStatus.LOADING) { return ( 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 3edd8f2514e63..e4b5e3b95c3f3 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,32 +10,31 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ReactWrapper } from 'enzyme'; import { ContextErrorMessage } from './context_error_message'; -// @ts-ignore -import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; +import { FailureReason, LoadingStatus } from '../../angular/context_app_state'; import { findTestSubject } from '@elastic/eui/lib/test'; describe('loading spinner', function () { let component: ReactWrapper; it('ContextErrorMessage does not render on loading', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); }); it('ContextErrorMessage does not render on success loading', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); }); it('ContextErrorMessage renders just the title if the reason is not specifically handled', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); expect(findTestSubject(component, 'contextErrorMessageBody').text()).toBe(''); }); it('ContextErrorMessage renders the reason for unknown errors', () => { component = mountWithIntl( - + ); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); expect(findTestSubject(component, 'contextErrorMessageBody').length).toBe(1); 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 83cb6981d761e..85dc67e7029ee 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,8 +9,7 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -// @ts-ignore -import { FAILURE_REASONS, LOADING_STATUS } from '../../angular/context/query'; +import { FailureReason, LoadingStatus } from '../../angular/context_app_state'; export interface ContextErrorMessageProps { /** @@ -24,7 +23,7 @@ export interface ContextErrorMessageProps { } export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps) { - if (status !== LOADING_STATUS.FAILED) { + if (status !== LoadingStatus.FAILED) { return null; } return ( @@ -41,7 +40,7 @@ export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps data-test-subj="contextErrorMessageTitle" > - {reason === FAILURE_REASONS.UNKNOWN && ( + {reason === FailureReason.UNKNOWN && ( Date: Tue, 4 May 2021 16:01:43 +0300 Subject: [PATCH 04/36] [Discover] use unknown instead of any, correct types --- .../angular/context/query_parameters/actions.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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 03372c4fa2ccf..02eab422b68a4 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 @@ -8,8 +8,13 @@ import { pick } from 'lodash'; -import { esFilters, Filter, IFieldType } from '../../../../../../data/public'; -import { FilterManager, IndexPatternsContract } from '../../../../../../data/public'; +import { + IndexPatternsContract, + FilterManager, + esFilters, + Filter, + IndexPatternField, +} from '../../../../../../data/public'; import { popularizeField } from '../../../helpers/popularize_field'; import { ContextAppState, QueryParameters } from '../../context_app_state'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; @@ -43,8 +48,8 @@ export function getQueryParameterActions( }; const addFilter = (state: ContextAppState) => async ( - field: string | IFieldType, - values: any, + field: IndexPatternField | string, + values: unknown, operation: string ) => { const indexPatternId = state.queryParameters.indexPatternId; @@ -58,7 +63,8 @@ export function getQueryParameterActions( filterManager.addFilters(newFilters); if (indexPatterns) { const indexPattern = await indexPatterns.get(indexPatternId); - await popularizeField(indexPattern, (field as IFieldType).name, indexPatterns); + const fieldName = typeof field === 'string' ? field : field.name; + await popularizeField(indexPattern, fieldName, indexPatterns); } }; From 902473aaa38c13ef08d99b0676d089e41f76931a Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 5 May 2021 17:26:44 +0300 Subject: [PATCH 05/36] [Discover] skip any type for tests --- .../public/application/angular/context/api/_stubs.ts | 1 + .../application/angular/context/api/anchor.test.ts | 1 + .../angular/context/api/context.predecessors.test.ts | 10 ++++++---- .../angular/context/api/context.successors.test.ts | 10 ++++++---- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/_stubs.ts b/src/plugins/discover/public/application/angular/context/api/_stubs.ts index 3736065492e06..241d0a621f245 100644 --- a/src/plugins/discover/public/application/angular/context/api/_stubs.ts +++ b/src/plugins/discover/public/application/angular/context/api/_stubs.ts @@ -34,6 +34,7 @@ export function createIndexPatternsStub() { * A stubbed search source with a `fetch` method that returns all of `_stubHits`. */ export function createSearchSourceStub(hits: EsHitRecordList, timeField?: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const searchSourceStub: any = { _stubHits: hits, _stubTimeField: timeField, diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 5f63ac1592a7c..62c9a2a5e3b90 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -16,6 +16,7 @@ describe('context app', function () { anchorId: string, sort: EsQuerySortValue[] ) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any let searchSourceStub: any; describe('function fetchAnchor', function () { diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index ec567b40f8eb0..dc097bc110e20 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -13,6 +13,7 @@ import { EsHitRecordList, fetchContextProvider } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; import { AnchorHitRecord } from './anchor'; import { Query } from '../../../../../../data/public'; +import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -37,13 +38,14 @@ describe('context app', function () { tieBreakerValue: number, size: number ) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; describe('function fetchPredecessors', function () { beforeEach(() => { mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -51,7 +53,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchPredecessors = ( indexPatternId, @@ -235,7 +237,7 @@ describe('context app', function () { beforeEach(() => { mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -243,7 +245,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchPredecessors = ( indexPatternId, diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index 987c56514b77c..f8fc7eb343206 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -14,6 +14,7 @@ import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { EsHitRecordList, fetchContextProvider } from './context'; import { AnchorHitRecord } from './anchor'; +import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -37,13 +38,14 @@ describe('context app', function () { tieBreakerValue: number, size: number ) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; describe('function fetchSuccessors', function () { beforeEach(() => { mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -51,7 +53,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchSuccessors = ( indexPatternId, @@ -238,7 +240,7 @@ describe('context app', function () { beforeEach(() => { mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -246,7 +248,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchSuccessors = ( indexPatternId, From 89f10ba083c2a3f624bc4d7c107072dc82098723 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 6 May 2021 10:40:32 +0300 Subject: [PATCH 06/36] [Discover] add euiDataGrid view --- .../application/angular/context_app.html | 4 +- .../public/application/angular/context_app.js | 7 +- .../context_app/context_app_legacy.scss | 16 +++++ .../context_app/context_app_legacy.tsx | 64 ++++++++++++++++--- .../context_app_legacy_directive.ts | 1 + 5 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 src/plugins/discover/public/application/components/context_app/context_app_legacy.scss diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 3d731459ad8d7..c8d215c00894b 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -3,6 +3,7 @@ filter="contextApp.actions.addFilter" hits="contextApp.state.rows.all" index-pattern="contextApp.indexPattern" + opts="contextApp.opts" sorting="contextApp.state.queryParameters.sort" columns="contextApp.state.queryParameters.columns" minimum-visible-rows="contextApp.state.rows.all.length" @@ -18,5 +19,4 @@ successor-status="contextApp.state.loadingStatus.successors.status" on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" use-new-fields-api="contextApp.state.useNewFieldsApi" - top-nav-menu="contextApp.topNavMenu" -> + top-nav-menu="contextApp.topNavMenu"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index a90904fa2ccea..ffc1fde5dc8c6 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -44,7 +44,8 @@ getAngularModule().directive('contextApp', function ContextApp() { }); function ContextAppController($scope, Private) { - const { filterManager, indexPatterns, uiSettings, navigation } = getServices(); + const services = getServices(); + const { filterManager, indexPatterns, uiSettings, navigation } = services; const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns); const queryActions = Private(QueryActionsProvider); @@ -55,6 +56,10 @@ function ContextAppController($scope, Private) { ); this.state.useNewFieldsApi = useNewFieldsApi; this.topNavMenu = navigation.ui.TopNavMenu; + this.opts = { + config: uiSettings, + services, + }; this.actions = _.mapValues( { diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss new file mode 100644 index 0000000000000..fbddc2b4f9919 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -0,0 +1,16 @@ +@import '../../../../../../core/public/mixins'; + +.dscSurrDocsPage { + @include kibanaFullBodyHeight(52px); // action bar height +} + +.dscSurrDocsContent { + display: flex; + flex-direction: column; + height: 100%; +} + +.dscSurrDocsGrid { + flex: 1 1 100%; + overflow: hidden; +} 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 55c2208105f13..d5a467c22624e 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,9 +6,11 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState } from 'react'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import './context_app_legacy.scss'; import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { IUiSettingsClient } from 'kibana/public'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, @@ -18,15 +20,23 @@ import { IIndexPattern, IndexPatternField } from '../../../../../data/common/ind import { LoadingStatus } from '../../angular/context_app_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'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { DiscoverServices } from '../../../build_services'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; - hits: Array>; + hits: ElasticSearchHit[]; indexPattern: IIndexPattern; + opts: { + config: IUiSettingsClient; + services: DiscoverServices; + }; filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; minimumVisibleRows: number; - sorting: string[]; + sorting: SortPairArr[]; status: string; reason: string; defaultStepSize: number; @@ -41,6 +51,7 @@ export interface ContextAppProps { useNewFieldsApi?: boolean; } +const DataGridMemoized = React.memo(DiscoverGrid); const PREDECESSOR_TYPE = 'predecessors'; const SUCCESSOR_TYPE = 'successors'; @@ -49,9 +60,11 @@ function isLoading(status: string) { } export function ContextAppLegacy(renderProps: ContextAppProps) { + const [expandedDoc, setExpandedDoc] = useState(undefined); const status = renderProps.status; const isLoaded = status === LoadingStatus.LOADED; const isFailed = status === LoadingStatus.FAILED; + const isLegacy = renderProps.opts.config.get('doc_table:legacy'); const actionBarProps = (type: string) => { const { @@ -78,6 +91,37 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const docTableProps = () => { + const { + columns, + hits, + filter, + indexPattern, + sorting, + opts: { config, services }, + useNewFieldsApi, + } = renderProps; + return { + ariaLabelledBy: 'surDocumentsAriaLabel', + columns, + rows: hits, + indexPattern, + expandedDoc, + isLoading: isLoading(status), + sampleSize: 0, + sort: sorting, + showTimeCol: !config.get('doc_table:hideTimeColumn', false) && !!indexPattern.timeFieldName, + services, + useNewFieldsApi, + setExpandedDoc, + onFilter: filter, + onAddColumn: (column) => {}, + onRemoveColumn: (column) => {}, + onSetColumns: (columnsToSet) => {}, + onSort: (sort) => {}, + } as DiscoverGridProps; + }; + + const legacyDocTableProps = () => { const { hits, filter, @@ -131,16 +175,20 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { ) : (
- - + + {loadingFeedback()} - {isLoaded ? ( + {isLegacy ? (
- + +
+ ) : ( +
+
- ) : null} + )}
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 fc64abfb51025..2335abc6b6e20 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,6 +14,7 @@ export function createContextAppLegacy(reactDirective: any) { ['filter', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], + ['opts', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], From 35abe5be451c6c0d4358344ddc087b92d741776b Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 7 May 2021 16:46:41 +0300 Subject: [PATCH 07/36] [Discover] add support dataGrid columns, provide ability to do not change sorting, highlight anchor doc, rename legacy variables --- .../public/application/angular/context.html | 5 +- .../public/application/angular/context.js | 18 +-- .../angular/context/api/anchor.test.ts | 7 +- .../application/angular/context/api/anchor.ts | 12 +- .../context/api/context.predecessors.test.ts | 6 +- .../context/api/context.successors.test.ts | 6 +- .../angular/context/api/context.ts | 4 +- .../angular/context/query/actions.tsx | 9 +- .../context/query_parameters/actions.test.ts | 7 +- .../application/angular/context_app.html | 3 +- .../public/application/angular/context_app.js | 10 +- .../application/angular/context_app_state.ts | 6 +- .../application/angular/context_state.ts | 8 +- .../angular/doc_table/actions/columns.ts | 13 +- .../angular/doc_table/doc_table.html | 4 +- .../context_app/context_app_legacy.scss | 10 +- .../context_app/context_app_legacy.tsx | 113 ++++++++++-------- .../context_app_legacy_directive.ts | 3 +- .../application/components/discover.tsx | 34 ++---- .../discover_grid/discover_grid.scss | 4 + .../discover_grid/discover_grid.tsx | 39 ++++-- .../discover_grid/discover_grid_columns.tsx | 12 +- .../discover_grid_document_selection.tsx | 13 +- .../discover_grid_expand_button.tsx | 6 +- .../discover_grid/get_render_cell_value.tsx | 6 +- .../application/doc_views/doc_views_types.ts | 4 +- .../helpers/use_data_grid_columns.ts | 70 +++++++++++ 27 files changed, 274 insertions(+), 158 deletions(-) create mode 100644 src/plugins/discover/public/application/helpers/use_data_grid_columns.ts diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index 2c8e9a2a5d6f0..adafb3a62275f 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -2,8 +2,9 @@ anchor-id="contextAppRoute.anchorId" columns="contextAppRoute.state.columns" index-pattern="contextAppRoute.indexPattern" + app-state="contextAppRoute.state" + state-container="contextAppRoute.stateContainer" filters="contextAppRoute.filters" predecessor-count="contextAppRoute.state.predecessorCount" successor-count="contextAppRoute.state.successorCount" - sort="contextAppRoute.state.sort" -> + sort="contextAppRoute.state.sort"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index 01a28a5c174b6..fb3493475b699 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -51,6 +51,14 @@ getAngularModule().config(($routeProvider) => { function ContextAppRouteController($routeParams, $scope, $route) { 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, + }); const { startSync: startStateSync, stopSync: stopStateSync, @@ -59,14 +67,8 @@ function ContextAppRouteController($routeParams, $scope, $route) { setFilters, setAppState, flushToUrl, - } = 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, - }); + } = stateContainer; + this.stateContainer = stateContainer; this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 62c9a2a5e3b90..6580e7310dbf8 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -8,14 +8,15 @@ import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; -import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; +import { fetchAnchorProvider } from './anchor'; +import { EsHitRecord } from './context'; describe('context app', function () { let fetchAnchor: ( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ) => Promise; + ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let searchSourceStub: any; @@ -139,7 +140,7 @@ describe('context app', function () { { _doc: SortDirection.desc }, ]).then((anchorDocument) => { expect(anchorDocument).toHaveProperty('property1', 'value1'); - expect(anchorDocument).toHaveProperty('$$_isAnchor', true); + expect(anchorDocument).toHaveProperty('isAnchor', true); }); }); }); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/angular/context/api/anchor.ts index da81ce525331a..f2111d020aade 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -16,11 +16,6 @@ import { } from '../../../../../../data/public'; import { EsHitRecord } from './context'; -export interface AnchorHitRecord extends EsHitRecord { - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: boolean; -} - export function fetchAnchorProvider( indexPatterns: IndexPatternsContract, searchSource: ISearchSource, @@ -30,7 +25,7 @@ export function fetchAnchorProvider( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ): Promise { + ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) @@ -66,8 +61,7 @@ export function fetchAnchorProvider( return { ...get(response, ['hits', 'hits', 0]), - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: true, - } as AnchorHitRecord; + isAnchor: true, + } as EsHitRecord; }; } diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index dc097bc110e20..1acf57411c795 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -11,7 +11,7 @@ import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { EsHitRecordList, fetchContextProvider } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -267,7 +267,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index f8fc7eb343206..957a13e8daf09 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -13,7 +13,7 @@ import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { EsHitRecordList, fetchContextProvider } from './context'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -270,7 +270,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, 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 4309b9ca4c391..56d28a59db251 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -14,7 +14,6 @@ import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; export type SurrDocType = 'successors' | 'predecessors'; export interface EsHitRecord { @@ -24,6 +23,7 @@ export interface EsHitRecord { // eslint-disable-next-line @typescript-eslint/no-explicit-any _source: Record; _id: string; + isAnchor?: boolean; } export type EsHitRecordList = EsHitRecord[]; @@ -53,7 +53,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields async function fetchSurroundingDocs( type: SurrDocType, indexPatternId: string, - anchor: AnchorHitRecord, + anchor: EsHitRecord, timeField: string, tieBreakerField: string, sortDir: SortDirection, 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 52c56d379d259..f79c28bf6a120 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../../../kibana_services'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; -import { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor'; +import { fetchAnchorProvider } from '../api/anchor'; import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; import { @@ -77,11 +77,12 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)('anchor'); + const [[, sortDir]] = sort; return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [fromPairs([sort]), { [tieBreakerField]: sort[1] }]) + fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) ).then( - (anchorDocument: AnchorHitRecord) => { + (anchorDocument: EsHitRecord) => { setLoadedStatus(state)('anchor'); state.rows.anchor = anchorDocument; return anchorDocument; @@ -120,7 +121,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)(type); - const [sortField, sortDir] = sort; + const [[sortField, sortDir]] = sort; return Promise.try(() => fetchSurroundingDocs( 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 b54f11e9e6706..0e7e0e3fd92d5 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 @@ -29,7 +29,7 @@ beforeEach(() => { anchorId: '', columns: [], filters: [], - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }, loadingStatus: { @@ -39,8 +39,7 @@ beforeEach(() => { }, rows: { all: [], - // eslint-disable-next-line @typescript-eslint/naming-convention - anchor: { $$_isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, + anchor: { isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, predecessors: [], successors: [], }, @@ -129,7 +128,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index c8d215c00894b..004dec81cb56c 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -3,7 +3,8 @@ filter="contextApp.actions.addFilter" hits="contextApp.state.rows.all" index-pattern="contextApp.indexPattern" - opts="contextApp.opts" + 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" diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index ffc1fde5dc8c6..7c9c5f8ce4b42 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -34,6 +34,8 @@ getAngularModule().directive('contextApp', function ContextApp() { anchorId: '=', columns: '=', indexPattern: '=', + appState: '=', + stateContainer: '=', filters: '=', predecessorCount: '=', successorCount: '=', @@ -44,8 +46,7 @@ getAngularModule().directive('contextApp', function ContextApp() { }); function ContextAppController($scope, Private) { - const services = getServices(); - const { filterManager, indexPatterns, uiSettings, navigation } = services; + const { filterManager, indexPatterns, uiSettings, navigation } = getServices(); const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns); const queryActions = Private(QueryActionsProvider); @@ -56,11 +57,6 @@ function ContextAppController($scope, Private) { ); this.state.useNewFieldsApi = useNewFieldsApi; this.topNavMenu = navigation.ui.TopNavMenu; - this.opts = { - config: uiSettings, - services, - }; - this.actions = _.mapValues( { ...queryParameterActions, diff --git a/src/plugins/discover/public/application/angular/context_app_state.ts b/src/plugins/discover/public/application/angular/context_app_state.ts index 1593b2457019c..0d9d6d6ea5978 100644 --- a/src/plugins/discover/public/application/angular/context_app_state.ts +++ b/src/plugins/discover/public/application/angular/context_app_state.ts @@ -7,7 +7,7 @@ */ import { Filter } from '../../../../data/public'; -import { AnchorHitRecord } from './context/api/anchor'; +import { EsHitRecord } from './context/api/context'; import { EsHitRecordList } from './context/api/context'; import { SortDirection } from './context/api/utils/sorting'; @@ -48,13 +48,13 @@ export interface QueryParameters { indexPatternId: string; predecessorCount: number; successorCount: number; - sort: [string, SortDirection]; + sort: Array<[string, SortDirection]>; tieBreakerField: string; } interface ContextRows { all: EsHitRecordList; - anchor: AnchorHitRecord; + anchor: EsHitRecord; predecessors: EsHitRecordList; successors: EsHitRecordList; } diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index d60f2e655c4eb..9cfea7f01e4ab 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -35,7 +35,7 @@ export interface AppState { /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ - sort: string[]; + sort: string[][]; /** * Number of records to be fetched after the anchor records (older records) */ @@ -50,7 +50,7 @@ interface GlobalState { filters: Filter[]; } -interface GetStateParams { +export interface GetStateParams { /** * Number of records to be fetched when 'Load' link/button is clicked */ @@ -81,7 +81,7 @@ interface GetStateParams { uiSettings: IUiSettingsClient; } -interface GetStateReturn { +export interface GetStateReturn { /** * Global state, the _g part of the URL */ @@ -276,7 +276,7 @@ function createInitialAppState( columns: ['_source'], filters: [], predecessorCount: parseInt(defaultSize, 10), - sort: [timeFieldName, 'desc'], + sort: [[timeFieldName, 'desc']], successorCount: parseInt(defaultSize, 10), }; if (typeof urlState !== 'object') { diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 8028aa6c08634..0907844aa1c54 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -8,7 +8,14 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { popularizeField } from '../../../helpers/popularize_field'; import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; -import { AppState } from '../../discover_state'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../../discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../../context_state'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; /** @@ -67,8 +74,8 @@ export function getStateColumnActions({ indexPattern: IndexPattern; indexPatterns: IndexPatternsContract; useNewFieldsApi: boolean; - setAppState: (state: Partial) => void; - state: AppState; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; }) { function onAddColumn(columnName: string) { if (capabilities.discover.save) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html index 4f297643a28f7..ecd7aa8f3dcf4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.html @@ -95,8 +95,8 @@ index-pattern="indexPattern" filter="filter" class="kbnDocTable__row" - ng-class="{'kbnDocTable__row--highlight': row['$$_isAnchor']}" - data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}" + ng-class="{'kbnDocTable__row--highlight': row['isAnchor']}" + data-test-subj="docTableRow{{ row['isAnchor'] ? ' docTableAnchorRow' : ''}}" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" use-new-fields-api="useNewFieldsApi" diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss index fbddc2b4f9919..b1991a8f440a1 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -1,16 +1,16 @@ @import '../../../../../../core/public/mixins'; -.dscSurrDocsPage { - @include kibanaFullBodyHeight(52px); // action bar height +.dscDocsPage { + @include kibanaFullBodyHeight(54px); // action bar height } -.dscSurrDocsContent { +.dscDocsContent { display: flex; flex-direction: column; height: 100%; } -.dscSurrDocsGrid { +.dscDocsGrid { flex: 1 1 100%; - overflow: hidden; + overflow: auto; } 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 d5a467c22624e..0dd3c23d6b29d 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 @@ -7,36 +7,35 @@ */ import React, { useState } from 'react'; +import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import './context_app_legacy.scss'; import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; -import { IUiSettingsClient } from 'kibana/public'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; -import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; import { LoadingStatus } from '../../angular/context_app_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'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; -import { DiscoverServices } from '../../../build_services'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { getServices, SortDirection } from '../../../kibana_services'; +import { GetStateReturn, AppState } from '../../angular/context_state'; +import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; hits: ElasticSearchHit[]; - indexPattern: IIndexPattern; - opts: { - config: IUiSettingsClient; - services: DiscoverServices; - }; - filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + indexPattern: IndexPattern; + appState: AppState; + stateContainer: GetStateReturn; + filter: DocViewFilterFn; minimumVisibleRows: number; - sorting: SortPairArr[]; + sorting: Array<[string, SortDirection]>; status: string; reason: string; defaultStepSize: number; @@ -48,7 +47,7 @@ export interface ContextAppProps { onChangeSuccessorCount: (count: number) => void; predecessorStatus: string; successorStatus: string; - useNewFieldsApi?: boolean; + useNewFieldsApi: boolean; } const DataGridMemoized = React.memo(DiscoverGrid); @@ -60,11 +59,39 @@ function isLoading(status: string) { } export function ContextAppLegacy(renderProps: ContextAppProps) { + const services = getServices(); + const { uiSettings: config, capabilities, indexPatterns } = services; + const { + indexPattern, + status: anchorStatus, + predecessorStatus, + successorStatus, + stateContainer, + appState: state, + useNewFieldsApi, + hits: rows, + sorting, + filter, + minimumVisibleRows, + } = renderProps; const [expandedDoc, setExpandedDoc] = useState(undefined); - const status = renderProps.status; - const isLoaded = status === LoadingStatus.LOADED; - const isFailed = status === LoadingStatus.FAILED; - const isLegacy = renderProps.opts.config.get('doc_table:legacy'); + const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const isFailed = anchorStatus === LoadingStatus.FAILED; + const allRowsLoaded = + anchorStatus === LoadingStatus.LOADED && + predecessorStatus === LoadingStatus.LOADED && + successorStatus === LoadingStatus.LOADED; + const isLegacy = config.get('doc_table:legacy'); + + const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState: stateContainer.setAppState, + state, + useNewFieldsApi, + }); const actionBarProps = (type: string) => { const { @@ -73,8 +100,6 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { predecessorCount, predecessorAvailable, successorAvailable, - predecessorStatus, - successorStatus, onChangePredecessorCount, onChangeSuccessorCount, } = renderProps; @@ -86,58 +111,42 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus), type, - isDisabled: !isLoaded, + isDisabled: !isAnchorLoaded, } as ActionBarProps; }; const docTableProps = () => { - const { - columns, - hits, - filter, - indexPattern, - sorting, - opts: { config, services }, - useNewFieldsApi, - } = renderProps; return { ariaLabelledBy: 'surDocumentsAriaLabel', columns, - rows: hits, + rows: allRowsLoaded && rows, indexPattern, expandedDoc, - isLoading: isLoading(status), + isLoading: !allRowsLoaded, sampleSize: 0, sort: sorting, + isSortEnabled: false, showTimeCol: !config.get('doc_table:hideTimeColumn', false) && !!indexPattern.timeFieldName, services, useNewFieldsApi, setExpandedDoc, onFilter: filter, - onAddColumn: (column) => {}, - onRemoveColumn: (column) => {}, - onSetColumns: (columnsToSet) => {}, - onSort: (sort) => {}, + onAddColumn, + onRemoveColumn, + onSetColumns, } as DiscoverGridProps; }; const legacyDocTableProps = () => { - const { - hits, - filter, - sorting, - columns, - indexPattern, - minimumVisibleRows, - useNewFieldsApi, - } = renderProps; // @ts-expect-error doesn't implement full DocTableLegacyProps interface return { columns, indexPattern, minimumVisibleRows, - rows: hits, + rows, onFilter: filter, + onAddColumn, + onRemoveColumn, sort: sorting.map((el) => [el]), useNewFieldsApi, } as DocTableLegacyProps; @@ -158,7 +167,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const loadingFeedback = () => { - if (status === LoadingStatus.UNINITIALIZED || status === LoadingStatus.LOADING) { + if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { return ( @@ -171,12 +180,12 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( {isFailed ? ( - + ) : ( -
+ - - + + {loadingFeedback()} @@ -185,7 +194,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) {
) : ( -
+
)} @@ -193,7 +202,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { -
+ )}
); 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 2335abc6b6e20..9ecdfab82a9a7 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,7 +14,8 @@ export function createContextAppLegacy(reactDirective: any) { ['filter', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], - ['opts', { watchDepth: 'reference' }], + ['appState', { watchDepth: 'reference' }], + ['stateContainer', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 90dfd2ef9dce9..89e736293b29e 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -36,7 +36,6 @@ import { DiscoverProps } from './types'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; import { DiscoverTopNav } from './discover_topnav'; @@ -44,6 +43,7 @@ import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { setBreadcrumbsTitle } from '../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; import { InspectorSession } from '../../../../inspector/public'; +import { useDataGridColumns } from '../helpers/use_data_grid_columns'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); const SidebarMemoized = React.memo(DiscoverSidebarResponsive); @@ -108,6 +108,16 @@ export function Discover({ [opts] ); + const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }); + useEffect(() => { const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; chrome.docTitle.change(`Discover${pageTitleSuffix}`); @@ -116,20 +126,6 @@ export function Discover({ addHelpMenuToAppChrome(chrome, docLinks); }, [savedSearch, chrome, docLinks]); - const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( - () => - getStateColumnActions({ - capabilities, - config, - indexPattern, - indexPatterns, - setAppState, - state, - useNewFieldsApi, - }), - [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] - ); - const onOpenInspector = useCallback(() => { // prevent overlapping setExpandedDoc(undefined); @@ -225,12 +221,6 @@ export function Discover({ } }; - const columns = useMemo(() => { - if (!state.columns) { - return []; - } - return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; - }, [state, useNewFieldsApi]); return ( @@ -444,8 +434,8 @@ export function Discover({ } services={services} settings={state.grid} - onAddColumn={onAddColumn} onFilter={onAddFilter as DocViewFilterFn} + onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index cb1b9a8ea191e..35cdb6c64f539 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -80,6 +80,10 @@ @include euiTextTruncate; } +.dscDiscoverGrid__cell--highlight { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); +} + .dscDiscoverGrid__descriptionListDescription { word-break: normal !important; } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index be38f166fa1c0..61653ba05ec90 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -88,9 +88,9 @@ export interface DiscoverGridProps { */ onSetColumns: (columns: string[]) => void; /** - * function to change sorting of the documents + * function to change sorting of the documents, skipped when isSortEnabled is set to false */ - onSort: (sort: string[][]) => void; + onSort?: (sort: string[][]) => void; /** * Array of documents provided by Elasticsearch */ @@ -123,6 +123,10 @@ export interface DiscoverGridProps { * Determines whether the time columns should be displayed (legacy settings) */ showTimeCol: boolean; + /** + * Manage user sorting control + */ + isSortEnabled?: boolean; /** * Current sort setting */ @@ -158,6 +162,7 @@ export const DiscoverGrid = ({ settings, showTimeCol, sort, + isSortEnabled = true, useNewFieldsApi, }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); @@ -210,9 +215,11 @@ export const DiscoverGrid = ({ const onTableSort = useCallback( (sortingColumnsData) => { - onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + if (isSortEnabled && onSort) { + onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + } }, - [onSort] + [onSort, isSortEnabled] ); /** @@ -237,8 +244,16 @@ export const DiscoverGrid = ({ const randomId = useMemo(() => htmlIdGenerator()(), []); const euiGridColumns = useMemo( - () => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns), - [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] + () => + getEuiGridColumns( + displayedColumns, + settings, + indexPattern, + showTimeCol, + defaultColumns, + isSortEnabled + ), + [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( @@ -250,10 +265,12 @@ export const DiscoverGrid = ({ }), [displayedColumns, indexPattern, showTimeCol, onSetColumns] ); - const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ - sortingColumns, - onTableSort, - ]); + const sorting = useMemo(() => { + if (isSortEnabled) { + return { columns: sortingColumns, onSort: onTableSort }; + } + return { columns: sortingColumns, onSort: () => {} }; + }, [sortingColumns, onTableSort, isSortEnabled]); const lead = useMemo(() => getLeadControlColumns(), []); const additionalControls = useMemo( @@ -332,10 +349,12 @@ export const DiscoverGrid = ({ ? { ...toolbarVisibility, showColumnSelector: false, + showSortSelector: isSortEnabled, additionalControls, } : { ...toolbarVisibility, + showSortSelector: isSortEnabled, additionalControls, } } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index df7e2285a0754..8f75ac2efe516 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -53,7 +53,8 @@ export function buildEuiGridColumn( columnName: string, columnWidth: number | undefined = 0, indexPattern: IndexPattern, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled?: boolean ) { const timeString = i18n.translate('discover.timeLabel', { defaultMessage: 'Time', @@ -62,7 +63,7 @@ export function buildEuiGridColumn( const column: EuiDataGridColumn = { id: columnName, schema: getSchemaByKbnType(indexPatternField?.type), - isSortable: indexPatternField?.sortable === true, + isSortable: isSortEnabled && indexPatternField?.sortable === true, display: columnName === '_source' ? i18n.translate('discover.grid.documentHeader', { @@ -100,7 +101,8 @@ export function getEuiGridColumns( settings: DiscoverGridSettings | undefined, indexPattern: IndexPattern, showTimeCol: boolean, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled?: boolean ) { const timeFieldName = indexPattern.timeFieldName; const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; @@ -108,12 +110,12 @@ export function getEuiGridColumns( if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { const usedColumns = [indexPattern.timeFieldName, ...columns]; return usedColumns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } return columns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index 4aaefc99479c1..bb47c85a01d01 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useState, useContext, useMemo } from 'react'; +import React, { useCallback, useState, useContext, useMemo, useEffect } from 'react'; import { EuiButtonEmpty, EuiContextMenuItem, @@ -13,6 +13,7 @@ import { EuiCopy, EuiPopover, EuiCheckbox, + EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import classNames from 'classnames'; @@ -27,12 +28,20 @@ export const getDocId = (doc: ElasticSearchHit & { _routing?: string }) => { const routing = doc._routing ? doc._routing : ''; return [doc._index, doc._id, routing].join('::'); }; -export const SelectButton = ({ rowIndex }: { rowIndex: number }) => { +export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { const ctx = useContext(DiscoverGridContext); const doc = useMemo(() => ctx.rows[rowIndex], [ctx.rows, rowIndex]); const id = useMemo(() => getDocId(doc), [doc]); const checked = useMemo(() => ctx.selectedDocs.includes(id), [ctx.selectedDocs, id]); + useEffect(() => { + if (doc.isAnchor) { + setCellProps({ + className: 'dscDiscoverGrid__cell--highlight', + }); + } + }, [doc, setCellProps]); + return ( { - if (expanded && current && expanded._id === current._id) { + if (current.isAnchor) { + setCellProps({ + className: 'dscDiscoverGrid__cell--highlight', + }); + } else if (expanded && current && expanded._id === current._id) { setCellProps({ style: { backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index fc3dd499f92e0..2090c68e233f9 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -38,7 +38,11 @@ export const getRenderCellValueFn = ( const ctx = useContext(DiscoverGridContext); useEffect(() => { - if (ctx.expanded && row && ctx.expanded._id === row._id) { + if (row?.isAnchor) { + setCellProps({ + className: 'dscDiscoverGrid__cell--highlight', + }); + } else if (ctx.expanded && row && ctx.expanded._id === row._id) { setCellProps({ style: { backgroundColor: ctx.isDarkMode diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 58399f31e032f..69b3c2cb51f58 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,7 +18,9 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; +export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number] & { + isAnchor?: boolean; +}; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts new file mode 100644 index 0000000000000..c913b9abd1b43 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -0,0 +1,70 @@ +/* + * 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 { useMemo } from 'react'; + +import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { IndexPattern, IndexPatternsContract } from '../../kibana_services'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../angular/discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../angular/context_state'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; + +interface UseDataGridColumnsProps { + capabilities: Capabilities; + config: IUiSettingsClient; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; +} + +export const useDataGridColumns = ({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, +}: UseDataGridColumnsProps) => { + const { onAddColumn, onRemoveColumn, onSetColumns, onMoveColumn } = useMemo( + () => + getStateColumnActions({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const columns = useMemo(() => { + if (!state.columns) { + return []; + } + return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; + }, [state, useNewFieldsApi]); + + return { + columns, + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +}; From 63493d523e201922a6e650d798aeb350f826ad11 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 7 May 2021 18:53:04 +0300 Subject: [PATCH 08/36] [Discover] update context_legacy test and types --- .../discover/public/__mocks__/ui_settings.ts | 4 +- .../application/angular/context_app.html | 4 +- .../context_app/context_app_legacy.scss | 12 +++--- .../context_app/context_app_legacy.test.tsx | 43 ++++++++++++++----- .../context_app/context_app_legacy.tsx | 33 +++++++------- .../context_app_legacy_directive.ts | 4 +- .../application/components/discover.tsx | 4 +- .../discover_grid_document_selection.test.tsx | 36 ++++++++++++++-- 8 files changed, 98 insertions(+), 42 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index e021a39a568e9..8347ff18edd7d 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common'; +import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -15,6 +15,8 @@ export const uiSettingsMock = ({ return 10; } else if (key === DEFAULT_COLUMNS_SETTING) { return ['default_column']; + } else if (key === DOC_TABLE_LEGACY) { + return true; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 004dec81cb56c..5ee1fac7a0bd7 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -8,8 +8,8 @@ sorting="contextApp.state.queryParameters.sort" columns="contextApp.state.queryParameters.columns" minimum-visible-rows="contextApp.state.rows.all.length" - status="contextApp.state.loadingStatus.anchor.status" - reason="contextApp.state.loadingStatus.anchor.reason" + anchor-status="contextApp.state.loadingStatus.anchor.status" + anchor-reason="contextApp.state.loadingStatus.anchor.reason" default-step-size="contextApp.state.queryParameters.defaultStepSize" predecessor-count="contextApp.state.queryParameters.predecessorCount" predecessor-available="contextApp.state.rows.predecessors.length" diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss index b1991a8f440a1..9f9c383c70d45 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -1,16 +1,16 @@ @import '../../../../../../core/public/mixins'; .dscDocsPage { - @include kibanaFullBodyHeight(54px); // action bar height + @include kibanaFullBodyHeight(54px); // action bar height } .dscDocsContent { - display: flex; - flex-direction: column; - height: 100%; + display: flex; + flex-direction: column; + height: 100%; } .dscDocsGrid { - flex: 1 1 100%; - overflow: auto; + flex: 1 1 100%; + overflow: auto; } diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 63845ab97b954..61d9c63852ea1 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -7,14 +7,34 @@ */ import React from 'react'; -import { ContextAppLegacy } from './context_app_legacy'; -import { IIndexPattern } from '../../../../../data/common/index_patterns'; import { mountWithIntl } from '@kbn/test/jest'; +import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; +import { ContextAppLegacy } from './context_app_legacy'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { ContextErrorMessage } from '../context_error_message'; import { TopNavMenuMock } from './__mocks__/top_nav_menu'; +import { AppState, GetStateReturn } from '../../angular/context_state'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { SortDirection } from 'src/plugins/data/common'; + +jest.mock('../../../kibana_services', () => { + return { + getServices: () => ({ + metadata: { + branch: 'test', + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + }), + }; +}); describe('ContextAppLegacy test', () => { const hit = { @@ -35,16 +55,18 @@ describe('ContextAppLegacy test', () => { }; const indexPattern = { id: 'test_index_pattern', - } as IIndexPattern; + } as IndexPattern; const defaultProps = { columns: ['_source'], filter: () => {}, - hits: [hit], - sorting: ['order_date', 'desc'], + hits: ([hit] as unknown) as ElasticSearchHit[], + sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>, minimumVisibleRows: 5, indexPattern, - status: 'loaded', - reason: 'no reason', + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorStatus: 'loaded', + anchorReason: 'no reason', defaultStepSize: 5, predecessorCount: 10, successorCount: 10, @@ -55,6 +77,7 @@ describe('ContextAppLegacy test', () => { predecessorStatus: 'loaded', successorStatus: 'loaded', topNavMenu: TopNavMenuMock, + useNewFieldsApi: false, }; const topNavProps = { appName: 'context', @@ -80,7 +103,7 @@ describe('ContextAppLegacy test', () => { it('renders loading indicator', () => { const props = { ...defaultProps }; - props.status = 'loading'; + props.anchorStatus = 'loading'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); @@ -91,8 +114,8 @@ describe('ContextAppLegacy test', () => { it('renders error message', () => { const props = { ...defaultProps }; - props.status = 'failed'; - props.reason = 'something went wrong'; + props.anchorStatus = 'failed'; + props.anchorReason = 'something went wrong'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); expect(component.find(TopNavMenuMock).length).toBe(0); 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 0dd3c23d6b29d..2df13f9859e94 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 @@ -11,6 +11,7 @@ import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import './context_app_legacy.scss'; import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, @@ -36,8 +37,10 @@ export interface ContextAppProps { filter: DocViewFilterFn; minimumVisibleRows: number; sorting: Array<[string, SortDirection]>; - status: string; - reason: string; + anchorStatus: string; + anchorReason: string; + predecessorStatus: string; + successorStatus: string; defaultStepSize: number; predecessorCount: number; successorCount: number; @@ -45,9 +48,7 @@ export interface ContextAppProps { successorAvailable: number; onChangePredecessorCount: (count: number) => void; onChangeSuccessorCount: (count: number) => void; - predecessorStatus: string; - successorStatus: string; - useNewFieldsApi: boolean; + useNewFieldsApi?: boolean; } const DataGridMemoized = React.memo(DiscoverGrid); @@ -63,16 +64,16 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { const { uiSettings: config, capabilities, indexPatterns } = services; const { indexPattern, - status: anchorStatus, + anchorStatus, predecessorStatus, successorStatus, + appState, stateContainer, - appState: state, - useNewFieldsApi, hits: rows, sorting, filter, minimumVisibleRows, + useNewFieldsApi, } = renderProps; const [expandedDoc, setExpandedDoc] = useState(undefined); const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; @@ -81,7 +82,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { anchorStatus === LoadingStatus.LOADED && predecessorStatus === LoadingStatus.LOADED && successorStatus === LoadingStatus.LOADED; - const isLegacy = config.get('doc_table:legacy'); + const isLegacy = config.get(DOC_TABLE_LEGACY); const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, @@ -89,8 +90,8 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { indexPattern, indexPatterns, setAppState: stateContainer.setAppState, - state, - useNewFieldsApi, + state: appState, + useNewFieldsApi: !!useNewFieldsApi, }); const actionBarProps = (type: string) => { @@ -180,7 +181,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( {isFailed ? ( - + ) : ( @@ -190,9 +191,11 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { {loadingFeedback()} {isLegacy ? ( -
- -
+ isAnchorLoaded && ( +
+ +
+ ) ) : (
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 9ecdfab82a9a7..04484c179ecb2 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 @@ -19,8 +19,8 @@ export function createContextAppLegacy(reactDirective: any) { ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['status', { watchDepth: 'reference' }], - ['reason', { watchDepth: 'reference' }], + ['anchorStatus', { watchDepth: 'reference' }], + ['anchorReason', { watchDepth: 'reference' }], ['defaultStepSize', { watchDepth: 'reference' }], ['predecessorCount', { watchDepth: 'reference' }], ['predecessorAvailable', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 89e736293b29e..87365565a9069 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,7 +34,7 @@ import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; @@ -96,7 +96,7 @@ export function Discover({ }, [opts.chartAggConfigs]); const contentCentered = resultState === 'uninitialized'; - const isLegacy = services.uiSettings.get('doc_table:legacy'); + const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const updateQuery = useCallback( (_payload, isUpdate?: boolean) => { diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx index 9ebe3ee95f797..41cf3f5a68edb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx @@ -51,7 +51,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -73,7 +80,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -95,7 +109,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -117,7 +138,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); From 18ad18141d13aa18a194f6fb2e90e0623da0dd4e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 12 May 2021 17:11:36 +0300 Subject: [PATCH 09/36] [Discover] update unit tests, add context header --- .../context/query_parameters/actions.test.ts | 2 +- .../context_app/context_app_legacy.tsx | 20 ++++++++++--- .../discover_grid_columns.test.tsx | 13 +++++++-- .../discover_grid/discover_grid_columns.tsx | 4 +-- .../discover_grid_document_selection.tsx | 28 +++++++++++++------ 5 files changed, 50 insertions(+), 17 deletions(-) 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 0e7e0e3fd92d5..83e44a5040fae 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 @@ -141,7 +141,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); }); 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 2df13f9859e94..0485763591435 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,11 @@ * Side Public License, v 1. */ -import React, { useState } from 'react'; +import React, { useState, Fragment } from 'react'; import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import './context_app_legacy.scss'; -import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { @@ -83,6 +83,7 @@ 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 { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, @@ -183,10 +184,21 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { {isFailed ? ( ) : ( - + + + + + + + + {loadingFeedback()} @@ -205,7 +217,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { - + )} ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 93b5bf8fde0c1..3cbac90aa39cb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -12,7 +12,14 @@ import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_ describe('Discover grid columns ', function () { it('returns eui grid columns without time column', async () => { - const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false); + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternMock, + false, + false, + true + ); expect(actual).toMatchInlineSnapshot(` Array [ Object { @@ -54,6 +61,7 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, false, + true, true ); expect(actual).toMatchInlineSnapshot(` @@ -94,7 +102,8 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, true, - false + false, + true ); expect(actual).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index 8f75ac2efe516..3a27772662b56 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -54,7 +54,7 @@ export function buildEuiGridColumn( columnWidth: number | undefined = 0, indexPattern: IndexPattern, defaultColumns: boolean, - isSortEnabled?: boolean + isSortEnabled: boolean ) { const timeString = i18n.translate('discover.timeLabel', { defaultMessage: 'Time', @@ -102,7 +102,7 @@ export function getEuiGridColumns( indexPattern: IndexPattern, showTimeCol: boolean, defaultColumns: boolean, - isSortEnabled?: boolean + isSortEnabled: boolean ) { const timeFieldName = indexPattern.timeFieldName; const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index bb47c85a01d01..bcbd1686ec05c 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -6,6 +6,7 @@ * Side Public License, v 1. */ import React, { useCallback, useState, useContext, useMemo, useEffect } from 'react'; +import classNames from 'classnames'; import { EuiButtonEmpty, EuiContextMenuItem, @@ -16,7 +17,8 @@ import { EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import classNames from 'classnames'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; @@ -29,18 +31,28 @@ export const getDocId = (doc: ElasticSearchHit & { _routing?: string }) => { return [doc._index, doc._id, routing].join('::'); }; export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { - const ctx = useContext(DiscoverGridContext); - const doc = useMemo(() => ctx.rows[rowIndex], [ctx.rows, rowIndex]); + const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } = useContext( + DiscoverGridContext + ); + const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]); const id = useMemo(() => getDocId(doc), [doc]); - const checked = useMemo(() => ctx.selectedDocs.includes(id), [ctx.selectedDocs, id]); + const checked = useMemo(() => selectedDocs.includes(id), [selectedDocs, id]); useEffect(() => { if (doc.isAnchor) { setCellProps({ className: 'dscDiscoverGrid__cell--highlight', }); + } else if (expanded && doc && expanded._id === doc._id) { + setCellProps({ + style: { + backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); } - }, [doc, setCellProps]); + }, [expanded, doc, setCellProps, isDarkMode]); return ( { if (checked) { - const newSelection = ctx.selectedDocs.filter((docId) => docId !== id); - ctx.setSelectedDocs(newSelection); + const newSelection = selectedDocs.filter((docId) => docId !== id); + setSelectedDocs(newSelection); } else { - ctx.setSelectedDocs([...ctx.selectedDocs, id]); + setSelectedDocs([...selectedDocs, id]); } }} /> From 233be90abe534dd2a11f4a426b2f1384f0bbb063 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 13 May 2021 15:27:05 +0300 Subject: [PATCH 10/36] [Discover] update unit and functional tests --- .../public/application/angular/context_state.test.ts | 10 ++++++---- test/functional/apps/discover/_data_grid_context.ts | 5 +++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index ed4a74c70112b..e9294567032c4 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -45,8 +45,10 @@ describe('Test Discover Context State', () => { "filters": Array [], "predecessorCount": 4, "sort": Array [ - "time", - "desc", + Array [ + "time", + "desc", + ], ], "successorCount": 4, } @@ -60,7 +62,7 @@ describe('Test Discover Context State', () => { state.setAppState({ predecessorCount: 10 }); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(time,desc),successorCount:4)"` + `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(!(time,desc)),successorCount:4)"` ); }); test('getState -> url to appState syncing', async () => { @@ -183,7 +185,7 @@ describe('Test Discover Context State', () => { `); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(time,desc),successorCount:4)"` + `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(!(time,desc)),successorCount:4)"` ); }); }); diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 275ac011820be..65b8f45d55750 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -67,9 +67,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.clickRowToggle({ rowIndex: 0 }); const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); await rowActions[1].click(); - // entering the context view (contains the legacy type) - const contextFields = await docTable.getFields(); + + const contextFields = await dataGrid.getFields(); const anchorTimestamp = contextFields[0][0]; + return anchorTimestamp === firstTimestamp; }); }); From 87809079cec141b7782438287e818671e6cca4fc Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 13 May 2021 17:55:00 +0300 Subject: [PATCH 11/36] [Discover] remove docTable from context test which uses new data grid --- test/functional/apps/discover/_data_grid_context.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 65b8f45d55750..ee60660ae4a9e 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const filterBar = getService('filterBar'); const dataGrid = getService('dataGrid'); - const docTable = getService('docTable'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -76,8 +75,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('should open the context view with the same columns', async () => { - const columnNames = await docTable.getHeaderFields(); - expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + const columnNames = await dataGrid.getHeaderFields(); + expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { @@ -112,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await browser.getCurrentUrl()).to.contain('#/context'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('document table has a length of 6', async () => { - const nrOfDocs = (await docTable.getBodyRows()).length; + const nrOfDocs = (await dataGrid.getBodyRows()).length; return nrOfDocs === 6; }); }); From 6388555e8a3f20b6cce3f32ad7a0fe55de3b7a54 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 21 May 2021 13:41:04 +0300 Subject: [PATCH 12/36] [Discover] update EsHitRecord type, use it for context app. add no pagination support --- .../angular/context/api/anchor.test.ts | 10 +++----- .../angular/context/api/context.ts | 15 +++++------ .../api/utils/get_es_query_search_after.ts | 10 ++++---- .../context_app/context_app_legacy.test.tsx | 5 ++-- .../context_app/context_app_legacy.tsx | 12 +++++---- .../application/components/discover.tsx | 8 ++++-- .../discover_grid/discover_grid.tsx | 25 ++++++++++++------- .../discover_grid_document_selection.tsx | 3 ++- .../discover_grid_expand_button.tsx | 3 ++- .../discover_grid/get_render_cell_value.tsx | 3 ++- .../application/doc_views/doc_views_types.ts | 4 +-- .../embeddable/search_embeddable.ts | 3 ++- 12 files changed, 55 insertions(+), 46 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 6580e7310dbf8..4da8ddc798003 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -9,7 +9,7 @@ import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; import { fetchAnchorProvider } from './anchor'; -import { EsHitRecord } from './context'; +import { EsHitRecord, EsHitRecordList } from './context'; describe('context app', function () { let fetchAnchor: ( @@ -22,9 +22,7 @@ describe('context app', function () { describe('function fetchAnchor', function () { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); @@ -147,9 +145,7 @@ describe('context app', function () { describe('useNewFields API', () => { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); 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 56d28a59db251..44a420da8303c 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import { Filter, IndexPatternsContract, IndexPattern } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; @@ -16,15 +17,11 @@ import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; -export interface EsHitRecord { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: Record; - sort: number[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _source: Record; - _id: string; +export type EsHitRecord = Required & { + sort: Array; + _source: Record; isAnchor?: boolean; -} +}; export type EsHitRecordList = EsHitRecord[]; const DAY_MILLIS = 24 * 60 * 60 * 1000; @@ -71,7 +68,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields const timeValueMillis = nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0]; - const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir); + const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); let documents: EsHitRecordList = []; for (const interval of intervals) { diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index fb0e58832a202..1f745ab1b728e 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,11 +28,11 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue: string | number = afterTimeDoc.sort[0]; + let afterTimeValue = afterTimeDoc.sort[0]; if (nanoSeconds) { afterTimeValue = useNewFieldsApi - ? (afterTimeDoc.fields[timeFieldName] as Array)[0] - : (afterTimeDoc._source[timeFieldName] as string | number); + ? afterTimeDoc.fields[timeFieldName][0] + : afterTimeDoc._source[timeFieldName]; } return [afterTimeValue, afterTimeDoc.sort[1]]; } @@ -42,8 +42,8 @@ export function getEsQuerySearchAfter( searchAfter[0] = anchor.sort[0]; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi - ? (anchor.fields[timeFieldName] as Array)[0] - : (anchor._source[timeFieldName] as string | number); + ? anchor.fields[timeFieldName][0] + : anchor._source[timeFieldName]; } searchAfter[1] = anchor.sort[1]; return searchAfter; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 61d9c63852ea1..09ad7e952cf0f 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -17,8 +17,8 @@ import { ActionBar } from '../../angular/context/components/action_bar/action_ba import { ContextErrorMessage } from '../context_error_message'; import { TopNavMenuMock } from './__mocks__/top_nav_menu'; import { AppState, GetStateReturn } from '../../angular/context_state'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { SortDirection } from 'src/plugins/data/common'; +import { EsHitRecordList } from '../../angular/context/api/context'; jest.mock('../../../kibana_services', () => { return { @@ -59,7 +59,7 @@ describe('ContextAppLegacy test', () => { const defaultProps = { columns: ['_source'], filter: () => {}, - hits: ([hit] as unknown) as ElasticSearchHit[], + hits: ([hit] as unknown) as EsHitRecordList, sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>, minimumVisibleRows: 5, indexPattern, @@ -78,6 +78,7 @@ describe('ContextAppLegacy test', () => { successorStatus: 'loaded', topNavMenu: TopNavMenuMock, useNewFieldsApi: false, + isPaginationEnabled: false, }; const topNavProps = { appName: 'context', 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 0485763591435..c6edc8fe68b9d 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 @@ -11,7 +11,7 @@ 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 { DOC_TABLE_LEGACY } from '../../../../common'; +import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, @@ -22,15 +22,16 @@ import { LoadingStatus } from '../../angular/context_app_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'; -import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; 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'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; - hits: ElasticSearchHit[]; + hits: EsHitRecordList; indexPattern: IndexPattern; appState: AppState; stateContainer: GetStateReturn; @@ -75,7 +76,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { minimumVisibleRows, useNewFieldsApi, } = renderProps; - const [expandedDoc, setExpandedDoc] = useState(undefined); + const [expandedDoc, setExpandedDoc] = useState(undefined); const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; const isFailed = anchorStatus === LoadingStatus.FAILED; const allRowsLoaded = @@ -128,9 +129,10 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { sampleSize: 0, sort: sorting, isSortEnabled: false, - showTimeCol: !config.get('doc_table:hideTimeColumn', false) && !!indexPattern.timeFieldName, + showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, services, useNewFieldsApi, + isPaginationEnabled: false, setExpandedDoc, onFilter: filter, onAddColumn, diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 87365565a9069..f962c56cc4690 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,7 +34,11 @@ import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; @@ -429,7 +433,7 @@ export function Discover({ searchTitle={opts.savedSearch.lastSavedTitle} setExpandedDoc={setExpandedDoc} showTimeCol={ - !config.get('doc_table:hideTimeColumn', false) && + !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName } services={services} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index e97924e52cc57..0f3ca11bde2c8 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -135,6 +135,10 @@ export interface DiscoverGridProps { * How the data is fetched */ useNewFieldsApi: boolean; + /** + * Manage pagination control + */ + isPaginationEnabled?: boolean; } export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { @@ -162,8 +166,9 @@ export const DiscoverGrid = ({ settings, showTimeCol, sort, - isSortEnabled = true, useNewFieldsApi, + isSortEnabled = true, + isPaginationEnabled = true, }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -215,14 +220,16 @@ export const DiscoverGrid = ({ const onChangePage = (pageIndex: number) => setPagination((paginationData) => ({ ...paginationData, pageIndex })); - return { - onChangeItemsPerPage, - onChangePage, - pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, - pageSize: pagination.pageSize, - pageSizeOptions: pageSizeArr, - }; - }, [pagination, pageCount]); + return isPaginationEnabled + ? { + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: pageSizeArr, + } + : undefined; + }, [pagination, pageCount, isPaginationEnabled]); /** * Sorting diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index bcbd1686ec05c..90566db002556 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -21,6 +21,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; +import { EsHitRecord } from '../../angular/context/api/context'; /** * Returning a generated id of a given ES document, since `_id` can be the same @@ -39,7 +40,7 @@ export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle const checked = useMemo(() => selectedDocs.includes(id), [selectedDocs, id]); useEffect(() => { - if (doc.isAnchor) { + if ((doc as EsHitRecord).isAnchor) { setCellProps({ className: 'dscDiscoverGrid__cell--highlight', }); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx index 985d9795875c3..d98c66290493f 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -12,6 +12,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; +import { EsHitRecord } from '../../angular/context/api/context'; /** * Button to expand a given row */ @@ -19,7 +20,7 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); const current = rows[rowIndex]; useEffect(() => { - if (current.isAnchor) { + if ((current as EsHitRecord).isAnchor) { setCellProps({ className: 'dscDiscoverGrid__cell--highlight', }); diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index 2090c68e233f9..997c62d6fe5cb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -21,6 +21,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; +import { EsHitRecord } from '../../angular/context/api/context'; export const getRenderCellValueFn = ( indexPattern: IndexPattern, @@ -38,7 +39,7 @@ export const getRenderCellValueFn = ( const ctx = useContext(DiscoverGridContext); useEffect(() => { - if (row?.isAnchor) { + if ((row as EsHitRecord).isAnchor) { setCellProps({ className: 'dscDiscoverGrid__cell--highlight', }); diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 69b3c2cb51f58..58399f31e032f 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -18,9 +18,7 @@ export interface AngularDirective { export type AngularScope = IScope; -export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number] & { - isAnchor?: boolean; -}; +export type ElasticSearchHit = estypes.SearchResponse['hits']['hits'][number]; export interface FieldMapping { filterable?: boolean; diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 99ecb4c11eef2..1e3c7e77d3615 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -33,6 +33,7 @@ import { getServices, IndexPattern, ISearchSource } from '../../kibana_services' import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; import { + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, @@ -256,7 +257,7 @@ export class SearchEmbeddable if (this.savedSearch.grid) { searchScope.settings = this.savedSearch.grid; } - searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false); + searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); searchScope.filter = async (field, value, operator) => { let filters = esFilters.generateFilters( From 720b613670707b837d1f8637672210e65a2753a8 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 21 May 2021 16:29:53 +0300 Subject: [PATCH 13/36] [Discover] resolve type error in test --- .../application/angular/context/api/context.ts | 12 +++++++++--- .../context/api/utils/get_es_query_search_after.ts | 12 ++++++------ .../angular/context/query_parameters/actions.test.ts | 3 ++- 3 files changed, 17 insertions(+), 10 deletions(-) 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 44a420da8303c..cd81ca7b216b2 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -17,11 +17,17 @@ import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; export type SurrDocType = 'successors' | 'predecessors'; -export type EsHitRecord = Required & { - sort: Array; - _source: Record; +export type EsHitRecord = Required< + Pick< + estypes.SearchResponse['hits']['hits'][number], + '_id' | 'fields' | 'sort' | '_index' | '_version' + > +> & { + _source?: Record; + _score?: number; isAnchor?: boolean; }; + export type EsHitRecordList = EsHitRecord[]; const DAY_MILLIS = 24 * 60 * 60 * 1000; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index 1f745ab1b728e..c703abaf2e523 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,23 +28,23 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue = afterTimeDoc.sort[0]; + let afterTimeValue = afterTimeDoc.sort[0] as string | number; if (nanoSeconds) { afterTimeValue = useNewFieldsApi ? afterTimeDoc.fields[timeFieldName][0] - : afterTimeDoc._source[timeFieldName]; + : afterTimeDoc._source?.[timeFieldName]; } - return [afterTimeValue, afterTimeDoc.sort[1]]; + return [afterTimeValue, afterTimeDoc.sort[1] as string | number]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string const searchAfter = new Array(2) as EsQuerySearchAfter; - searchAfter[0] = anchor.sort[0]; + searchAfter[0] = anchor.sort[0] as string | number; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi ? anchor.fields[timeFieldName][0] - : anchor._source[timeFieldName]; + : anchor._source?.[timeFieldName]; } - searchAfter[1] = anchor.sort[1]; + searchAfter[1] = anchor.sort[1] as string | number; return searchAfter; } 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 83e44a5040fae..fac3e1ea6fad6 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 @@ -10,6 +10,7 @@ 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 { EsHitRecord } from '../api/context'; const setupMock = coreMock.createSetup(); let state: ContextAppState; @@ -39,7 +40,7 @@ beforeEach(() => { }, rows: { all: [], - anchor: { isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, + anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord, predecessors: [], successors: [], }, From b81a4e2630a4cc2f35ce25cff455fef4432c88cc Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 25 May 2021 10:45:54 +0300 Subject: [PATCH 14/36] [Discover] move fetching methods --- .../public/application/angular/context.html | 1 + .../public/application/angular/context.js | 40 +++-- .../angular/context/api/context.ts | 5 +- .../angular/context/query/actions.tsx | 28 +-- .../angular/context/query/state.ts | 2 +- .../context/query_parameters/actions.test.ts | 4 +- .../context/query_parameters/actions.ts | 10 +- .../angular/context/query_parameters/state.ts | 10 +- .../application/angular/context_app.html | 2 + .../public/application/angular/context_app.js | 5 +- ...xt_app_state.ts => context_query_state.ts} | 37 +++- .../application/angular/context_state.ts | 38 +++-- .../context_app/context_app_legacy.tsx | 77 ++++++++- .../context_app_legacy_directive.ts | 2 + .../context_app/use_context_app_query.tsx | 161 ++++++++++++++++++ .../context_app/use_context_app_state.ts | 92 ++++++++++ .../context_error_message.test.tsx | 2 +- .../context_error_message.tsx | 2 +- 18 files changed, 448 insertions(+), 70 deletions(-) rename src/plugins/discover/public/application/angular/{context_app_state.ts => context_query_state.ts} (64%) create mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx create mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_state.ts 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 { /** From 7f69c92f9fc691d2b737bf78cef7cb2d98984c18 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 26 May 2021 03:06:53 +0300 Subject: [PATCH 15/36] [Discover] complete fetching part --- .../application/angular/context/api/anchor.ts | 14 +- .../angular/context_query_state.ts | 34 +++- .../application/angular/context_state.ts | 16 +- .../context_app/context_app_legacy.tsx | 89 ++++++--- .../context_app/use_context_app_query.tsx | 189 ++++++++++-------- 5 files changed, 211 insertions(+), 131 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/angular/context/api/anchor.ts index f2111d020aade..75d262dbfbbff 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -16,16 +16,18 @@ import { } from '../../../../../../data/public'; import { EsHitRecord } from './context'; +export type FetchAnchorCallback = ( + indexPatternId: string, + anchorId: string, + sort: EsQuerySortValue[] +) => Promise; + export function fetchAnchorProvider( indexPatterns: IndexPatternsContract, searchSource: ISearchSource, useNewFieldsApi: boolean = false -) { - return async function fetchAnchor( - indexPatternId: string, - anchorId: string, - sort: EsQuerySortValue[] - ): Promise { +): FetchAnchorCallback { + return async function fetchAnchor(indexPatternId, anchorId, sort) { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) 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 bf2b435d025b5..488e919804da4 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -17,6 +17,15 @@ export interface ContextQueryState { loadingStatus: LoadingStatusState; queryParameters: QueryParameters; rows: ContextRows; + hits: EsHitRecordList; + predecessors: EsHitRecordList; + successors: EsHitRecordList; + anchor: EsHitRecord; + anchorStatus: LoadingState; + predecessorsStatus: LoadingState; + successorsStatus: LoadingState; + predecessorCount: number; + successorCount: number; useNewFieldsApi: boolean; } @@ -42,6 +51,8 @@ export interface LoadingStatusState { successors: LoadingStatusEntry | LoadingStatus; } +export type LoadingState = LoadingStatusEntry | LoadingStatus; + export interface QueryParameters { anchorId: string; columns: string[]; @@ -54,11 +65,11 @@ export interface QueryParameters { tieBreakerField: string; } -interface ContextRows { +export interface ContextRows { all: EsHitRecordList; anchor: EsHitRecord; - // predecessors: EsHitRecordList; - // successors: EsHitRecordList; + predecessors: EsHitRecordList; + successors: EsHitRecordList; } export function getContextQueryDefaults( @@ -75,6 +86,14 @@ export function getContextQueryDefaults( defaultStepSize, tieBreakerField ), + hits: [], + predecessors: [], + successors: [], + anchor: { + fields: [], + sort: [], + _id: '', + }, rows: { all: [], anchor: { @@ -82,10 +101,15 @@ export function getContextQueryDefaults( sort: [], _id: '', }, - // predecessors: [], - // successors: [], + predecessors: [], + successors: [], }, loadingStatus: createInitialLoadingStatusState(), + anchorStatus: LoadingStatus.UNINITIALIZED, + predecessorsStatus: LoadingStatus.UNINITIALIZED, + successorsStatus: LoadingStatus.UNINITIALIZED, + predecessorCount: 5, + successorCount: 5, useNewFieldsApi, }; } diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 9b1a52cdb4e0a..304e83f774e2b 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -29,18 +29,18 @@ export interface AppState extends ContextQueryState { * Array of filters */ filters?: Filter[]; - /** - * Number of records to be fetched before anchor records (newer records) - */ - predecessorCount?: number; + // /** + // * Number of records to be fetched before anchor records (newer records) + // */ + // predecessorCount?: number; /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ sort: [[string, SortDirection]]; - /** - * Number of records to be fetched after the anchor records (older records) - */ - successorCount: number; + // /** + // * Number of records to be fetched after the anchor records (older records) + // */ + // successorCount: number; query?: Query; } 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 95fd080480603..4062edf3909b8 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,12 +6,14 @@ * Side Public License, v 1. */ -import React, { useState, Fragment, useEffect, useRef } from 'react'; +import React, { useState, Fragment, useEffect, useRef, useCallback } 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 { FilterManager } from 'src/plugins/data/public'; +import { map } from 'rxjs/operators'; import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { @@ -19,7 +21,7 @@ import { DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IndexPattern } from '../../../../../data/common/index_patterns'; -import { LoadingStatus } from '../../angular/context_query_state'; +import { LoadingState, 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'; @@ -31,6 +33,7 @@ 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'; +// import { fetchAnchorRow } from './context_app_actions'; export interface ContextAppProps { topNavMenu: React.ComponentType; @@ -62,7 +65,7 @@ const DataGridMemoized = React.memo(DiscoverGrid); const PREDECESSOR_TYPE = 'predecessors'; const SUCCESSOR_TYPE = 'successors'; -function isLoading(status: string) { +function isLoading(status: LoadingState) { return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; } @@ -76,9 +79,9 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { const { indexPattern, indexPatternId, - anchorStatus, - predecessorStatus, - successorStatus, + // anchorStatus, + // predecessorStatus, + // successorStatus, // hits: rows, sorting, anchorId, @@ -87,16 +90,16 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { useNewFieldsApi, } = renderProps; const [expandedDoc, setExpandedDoc] = useState(undefined); - const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; - const isFailed = anchorStatus === LoadingStatus.FAILED; - const allRowsLoaded = - anchorStatus === LoadingStatus.LOADED && - predecessorStatus === LoadingStatus.LOADED && - successorStatus === LoadingStatus.LOADED; + // const isAnchorLoaded = anchor === LoadingStatus.LOADED; + // const isFailed = anchorStatus === LoadingStatus.FAILED; + // const allRowsLoaded = + // anchorStatus === LoadingStatus.LOADED && + // predecessorStatus === LoadingStatus.LOADED && + // successorStatus === LoadingStatus.LOADED; const isLegacy = config.get(DOC_TABLE_LEGACY); // const anchorId = rows?.find(({ isAnchor }) => isAnchor)?._id; - const { state, stateContainer, setAppState } = useContextAppState({ + const { state, setAppState } = useContextAppState({ indexPattern, indexPatternId, anchorId, @@ -104,16 +107,22 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }); const prevState = useRef(); - useEffect(() => { - stateContainer.startSync(); + const isAnchorLoaded = state.anchorStatus === LoadingStatus.LOADED; + const isFailed = state.anchorStatus === LoadingStatus.FAILED; + const allRowsLoaded = + state.anchorStatus === LoadingStatus.LOADED && + state.predecessorsStatus === LoadingStatus.LOADED && + state.successorsStatus === LoadingStatus.LOADED; - return () => stateContainer.stopSync(); - }, [stateContainer]); + // useEffect(() => { + // stateContainer.startSync(); - const { context$, fetchAnchorRow, fetchAllRows } = useContextAppQuery({ + // return () => stateContainer.stopSync(); + // }, [stateContainer]); + + const { context$, fetchContextRows, fetchAllRows } = useContextAppQuery({ services, useNewFieldsApi: !!useNewFieldsApi, - state, }); /** @@ -121,30 +130,44 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { */ useEffect(() => { if (!prevState.current) { - fetchAllRows(); + fetchAllRows(state.predecessorCount, state.successorCount, { + indexPatternId, + anchorId, + tieBreakerField: state.queryParameters.tieBreakerField, + sort: state.sort, + }); } else if ( prevState.current.predecessorCount !== state.predecessorCount || prevState.current.successorCount !== state.successorCount || !isEqual(prevState.current.filters, state.filters) ) { - fetchAllRows(); + fetchContextRows(state.predecessorCount, state.successorCount, { + indexPatternId, + anchor: state.anchor, + tieBreakerField: state.queryParameters.tieBreakerField, + sort: state.sort, + }); } prevState.current = cloneDeep(state); - }, [state, fetchAllRows, fetchAnchorRow, indexPatternId]); + }, [state, indexPatternId, anchorId, fetchContextRows, fetchAllRows]); /** * Sync app state with context$ */ useEffect(() => { - context$.subscribe((next) => { - setAppState(next); + context$.subscribe((value) => { + setAppState(value); }); return () => context$.unsubscribe(); }, [context$, setAppState]); - const rows = state.rows; + const rows = [ + ...(state.predecessors || []), + ...(state.anchor ? [state.anchor] : []), + ...(state.successors || []), + ]; const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, config, @@ -171,7 +194,9 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { docCount: isPredecessorType ? predecessorCount : successorCount, docCountAvailable: isPredecessorType ? predecessorAvailable : successorAvailable, onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, - isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus), + isLoading: isPredecessorType + ? isLoading(state.predecessorsStatus) + : isLoading(state.successorsStatus), type, isDisabled: !isAnchorLoaded, } as ActionBarProps; @@ -181,7 +206,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return { ariaLabelledBy: 'surDocumentsAriaLabel', columns, - rows: rows.all.length !== 0 && rows.all, + rows: rows.length !== 0 && rows, indexPattern, expandedDoc, isLoading: !allRowsLoaded, @@ -230,7 +255,10 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const loadingFeedback = () => { - if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { + if ( + state.anchorStatus === LoadingStatus.UNINITIALIZED || + state.anchorStatus === LoadingStatus.LOADING + ) { return ( @@ -243,7 +271,10 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( {isFailed ? ( - + ) : ( 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 index 1463438c87df5..e1bd7fa4908fd 100644 --- 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 @@ -5,7 +5,7 @@ * 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 React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { fromPairs } from 'lodash'; @@ -20,16 +20,44 @@ import { SurrDocType, } from '../../angular/context/api/context'; import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; +import { Filter, SortDirection } from '../../../../../data/public'; +import { + ContextRows, + FailureReason, + LoadingState, + LoadingStatus, +} from '../../angular/context_query_state'; export type ContextAppMessage = Partial; +interface ContextDocsMessage { + successors?: EsHitRecordList; + predecessors?: EsHitRecordList; + anchor?: EsHitRecord; + all?: EsHitRecordList; + anchorStatus?: LoadingState; + predecessorsStatus?: LoadingState; + successorsStatus?: LoadingState; +} + +interface FetchAllRowsParams { + anchorId: string; + indexPatternId: string; + tieBreakerField: string; + sort: [[string, SortDirection]]; +} +interface FetchSurrDocsParams { + indexPatternId: string; + anchor: EsHitRecord; + tieBreakerField: string; + sort: [[string, SortDirection]]; +} + export function useContextAppQuery({ services, useNewFieldsApi, - state, }: { services: DiscoverServices; - state: AppState; useNewFieldsApi: boolean; }) { const { data, indexPatterns, toastNotifications, filterManager } = services; @@ -47,68 +75,55 @@ export function useContextAppQuery({ [indexPatterns, useNewFieldsApi] ); - const context$ = useMemo(() => new Subject(), []); + const context$ = useMemo(() => new Subject(), []); - const fetchAnchorRow = useCallback(() => { - const { - queryParameters: { indexPatternId, anchorId, tieBreakerField }, + const fetchAnchorRow = useCallback( + ({ + 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}), + }: { + indexPatternId: string; + anchorId: string; + tieBreakerField: string; + sort: [[string, SortDirection]]; + }) => { + const [[, sortDir]] = sort; + + context$.next({ anchorStatus: LoadingStatus.LOADING }); + return fetchAnchor(indexPatternId, anchorId, [ + fromPairs(sort), + { [tieBreakerField]: sortDir }, + ]) + .then( + (anchor: EsHitRecord): EsHitRecord => { + context$.next({ anchor, anchorStatus: LoadingStatus.LOADED }); + return anchor; + } + ) + .catch((error) => { + context$.next({ + anchorStatus: { reason: FailureReason.INVALID_TIEBREAKER }, + }); + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load the anchor document', + }), + text: toMountPoint({error.message}), + }); + throw error; }); - throw error; - } - ); - }, [fetchAnchor, state, toastNotifications, context$]); + }, + [context$, fetchAnchor, toastNotifications] + ); const fetchSurroundingRows = useCallback( - (type: SurrDocType) => { - const { - queryParameters: { indexPatternId, tieBreakerField }, - rows: { anchor }, - sort, - } = state; + (type, count, { indexPatternId, anchor, tieBreakerField, sort }: FetchSurrDocsParams) => { const filters = filterManager.getFilters(); - - const count = - type === 'successors' - ? state.queryParameters.successorCount - : state.queryParameters.predecessorCount; - - if (!tieBreakerField) { - // reject request - } - - // set loading const [[sortField, sortDir]] = sort; + context$.next({ [`${type}Status`]: LoadingStatus.LOADING }); return fetchSurroundingDocs( type, indexPatternId, @@ -119,12 +134,10 @@ export function useContextAppQuery({ count, filters ).then( - (documents: EsHitRecordList) => { - // set loaded - return documents; - }, - (error: Error) => { - // set error + (hits: EsHitRecordList) => + context$.next({ [type]: hits, [`${type}Status`]: LoadingStatus.LOADED }), + (error) => { + context$.next({ [`${type}Status`]: { reason: LoadingStatus.FAILED } }); toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', @@ -135,27 +148,37 @@ export function useContextAppQuery({ } ); }, - [toastNotifications, fetchSurroundingDocs, filterManager, state] + [context$, fetchSurroundingDocs, filterManager, toastNotifications] ); - 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$]); + const fetchContextRows = useCallback( + (predecessorCount: number, successorCount: number, params: FetchSurrDocsParams) => { + return Promise.all([ + fetchSurroundingRows('predecessors', predecessorCount, params), + fetchSurroundingRows('successors', successorCount, params), + ]); + }, + [fetchSurroundingRows] + ); + + const fetchAllRows = useCallback( + ( + predecessorCount: number, + successorCount: number, + { anchorId, ...restParams }: FetchAllRowsParams + ) => { + fetchAnchorRow({ + anchorId, + ...restParams, + }).then((anchor: EsHitRecord) => { + return fetchContextRows(predecessorCount, successorCount, { + anchor, + ...restParams, + }); + }); + }, + [fetchAnchorRow, fetchContextRows] + ); - return { context$, fetchAnchorRow, fetchAllRows }; + return { context$, fetchContextRows, fetchAllRows }; } From 43a9966fd0301a8f46126a2b3ad491109bdd36f1 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Mon, 31 May 2021 10:58:26 +0300 Subject: [PATCH 16/36] [Discover] remove redundant controller --- .../public/application/angular/context.html | 11 +- .../public/application/angular/context.js | 72 ------- .../application/angular/context_app.html | 25 --- .../public/application/angular/context_app.js | 130 ------------- .../angular/context_query_state.ts | 14 +- .../application/angular/context_state.ts | 52 +++-- .../context_app/context_app_legacy.tsx | 179 ++++++----------- .../context_app_legacy_directive.ts | 20 -- .../context_app/use_context_app_actions.tsx | 150 ++++++++++++++ .../context_app/use_context_app_query.tsx | 184 ------------------ .../context_app/use_context_app_state.ts | 32 ++- 11 files changed, 264 insertions(+), 605 deletions(-) delete mode 100644 src/plugins/discover/public/application/angular/context_app.html delete mode 100644 src/plugins/discover/public/application/angular/context_app.js create mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx delete mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index 9fe2708d84b69..b5254721a1e9d 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -1,11 +1,4 @@ - \ No newline at end of file + anchor-id="contextAppRoute.anchorId"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index f24454c1cc57e..e51863871b175 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -6,21 +6,10 @@ * Side Public License, v 1. */ -import _ from 'lodash'; import { i18n } from '@kbn/i18n'; -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; @@ -59,65 +48,4 @@ 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 stateContainer = getState({ - 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, - stopSync: stopStateSync, - appState, - getFilters, - setFilters, - setAppState, - flushToUrl, - } = stateContainer; - this.stateContainer = stateContainer; - this.state = { ...appState.getState() }; - filterManager.setFilters(_.cloneDeep(getFilters())); - startStateSync(); - - // take care of parameter changes in UI - $scope.$watchGroup( - [ - 'contextAppRoute.state.columns', - 'contextAppRoute.state.predecessorCount', - 'contextAppRoute.state.successorCount', - ], - (newValues) => { - const [columns, predecessorCount, successorCount] = newValues; - if (Array.isArray(columns) && predecessorCount >= 0 && successorCount >= 0) { - setAppState({ columns, predecessorCount, successorCount }); - flushToUrl(true); - } - } - ); - // take care of parameter filter changes - const filterObservable = filterManager.getUpdates$().subscribe(() => { - setFilters(filterManager); - $route.reload(); - }); - - $scope.$on('$destroy', () => { - stopStateSync(); - filterObservable.unsubscribe(); - }); } diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html deleted file mode 100644 index d5b7279e5dcec..0000000000000 --- a/src/plugins/discover/public/application/angular/context_app.html +++ /dev/null @@ -1,25 +0,0 @@ - - \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js deleted file mode 100644 index d868156b5e82d..0000000000000 --- a/src/plugins/discover/public/application/angular/context_app.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * 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 _ from 'lodash'; -import { - CONTEXT_STEP_SETTING, - CONTEXT_TIE_BREAKER_FIELDS_SETTING, - SEARCH_FIELDS_FROM_SOURCE, -} from '../../../common'; -import { getAngularModule, getServices } from '../../kibana_services'; -import contextAppTemplate from './context_app.html'; -import './context/components/action_bar'; -import { getFirstSortableField } from './context/api/utils/sorting'; -import { - createInitialQueryParametersState, - getQueryParameterActions, - QUERY_PARAMETER_KEYS, -} from './context/query_parameters'; -import { createInitialLoadingStatusState, QueryActionsProvider } from './context/query'; -import { callAfterBindingsWorkaround } from './context/helpers/call_after_bindings_workaround'; - -getAngularModule().directive('contextApp', function ContextApp() { - return { - bindToController: true, - controller: callAfterBindingsWorkaround(ContextAppController), - controllerAs: 'contextApp', - restrict: 'E', - scope: { - anchorId: '=', - columns: '=', - indexPattern: '=', - indexPatternId: '=', - appState: '=', - stateContainer: '=', - filters: '=', - predecessorCount: '=', - successorCount: '=', - sort: '=', - }, - template: contextAppTemplate, - }; -}); - -function ContextAppController($scope, Private) { - const { filterManager, indexPatterns, uiSettings, navigation } = getServices(); - const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); - const queryParameterActions = getQueryParameterActions(filterManager, indexPatterns); - const queryActions = Private(QueryActionsProvider); - this.state = createInitialState( - parseInt(uiSettings.get(CONTEXT_STEP_SETTING), 10), - getFirstSortableField(this.indexPattern, uiSettings.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)), - useNewFieldsApi - ); - this.state.useNewFieldsApi = useNewFieldsApi; - this.topNavMenu = navigation.ui.TopNavMenu; - this.actions = _.mapValues( - { - ...queryParameterActions, - ...queryActions, - }, - (action) => (...args) => action(this.state)(...args) - ); - - $scope.$watchGroup( - [ - () => this.state.rows.predecessors, - () => this.state.rows.anchor, - () => this.state.rows.successors, - ], - (newValues) => this.actions.setAllRows(...newValues) - ); - - /** - * Sync properties to state - */ - $scope.$watchCollection( - () => ({ - ..._.pick(this, QUERY_PARAMETER_KEYS), - indexPatternId: this.indexPattern.id, - }), - (newQueryParameters) => { - const { queryParameters } = this.state; - if ( - newQueryParameters.indexPatternId !== queryParameters.indexPatternId || - newQueryParameters.anchorId !== queryParameters.anchorId || - !_.isEqual(newQueryParameters.sort, queryParameters.sort) - ) { - // 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)); - } - } - ); - - /** - * Sync state to properties - */ - $scope.$watchCollection( - () => ({ - predecessorCount: this.state.queryParameters.predecessorCount, - successorCount: this.state.queryParameters.successorCount, - }), - (newParameters) => { - _.assign(this, newParameters); - } - ); -} - -function createInitialState(defaultStepSize, tieBreakerField, useNewFieldsApi) { - return { - queryParameters: createInitialQueryParametersState(defaultStepSize, tieBreakerField), - rows: { - all: [], - anchor: null, - predecessors: [], - successors: [], - }, - loadingStatus: createInitialLoadingStatusState(), - useNewFieldsApi, - }; -} 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 488e919804da4..aa5b46863460a 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -29,6 +29,12 @@ export interface ContextQueryState { useNewFieldsApi: boolean; } +export interface ContextQueryParams { + defaultStepSize: number; + tieBreakerField: string; + useNewFieldsApi: boolean; +} + export enum LoadingStatus { FAILED = 'failed', LOADED = 'loaded', @@ -46,12 +52,12 @@ export type LoadingStatusEntry = Partial<{ }>; export interface LoadingStatusState { - anchor: LoadingStatusEntry | LoadingStatus; - predecessors: LoadingStatusEntry | LoadingStatus; - successors: LoadingStatusEntry | LoadingStatus; + anchor: LoadingState; + predecessors: LoadingState; + successors: LoadingState; } -export type LoadingState = LoadingStatusEntry | LoadingStatus; +export type LoadingState = LoadingStatus | LoadingStatusEntry; export interface QueryParameters { anchorId: string; diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 304e83f774e2b..3026c4b2a54d6 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -16,32 +16,41 @@ import { withNotifyOnErrors, ReduxLikeStateContainer, } from '../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter, Query, SortDirection } from '../../../../data/public'; +import { esFilters, FilterManager, Filter, SortDirection } from '../../../../data/public'; import { handleSourceColumnState } from './helpers'; -import { ContextQueryState } from './context_query_state'; +import { ContextQueryParams, LoadingState, LoadingStatus } from './context_query_state'; +import { EsHitRecord, EsHitRecordList } from './context/api/context'; -export interface AppState extends ContextQueryState { +export interface AppState extends ContextQueryParams { /** * 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[]; - // /** - // * Number of records to be fetched before anchor records (newer records) - // */ - // predecessorCount?: number; + filters: Filter[]; + /** + * Number of records to be fetched before anchor records (newer records) + */ + predecessorCount: number; /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ sort: [[string, SortDirection]]; - // /** - // * Number of records to be fetched after the anchor records (older records) - // */ - // successorCount: number; - query?: Query; + /** + * Number of records to be fetched after the anchor records (older records) + */ + successorCount: number; + + hits: EsHitRecordList; + predecessors: EsHitRecordList; + successors: EsHitRecordList; + anchor: EsHitRecord; + + anchorStatus: LoadingState; + predecessorsStatus: LoadingState; + successorsStatus: LoadingState; } interface GlobalState { @@ -84,7 +93,7 @@ export interface GetStateParams { /** * Default state used for data querying */ - getContextQueryDefaults: () => ContextQueryState; + getContextQueryDefaults: () => ContextQueryParams; } export interface GetStateReturn { @@ -279,7 +288,7 @@ function createInitialAppState( timeFieldName: string, urlState: AppState, uiSettings: IUiSettingsClient, - getContextQueryDefaults: () => ContextQueryState + getContextQueryDefaults: () => ContextQueryParams ): AppState { const defaultState: AppState = { columns: ['_source'], @@ -287,6 +296,17 @@ function createInitialAppState( predecessorCount: defaultSize, sort: [[timeFieldName, SortDirection.desc]], successorCount: defaultSize, + hits: [], + predecessors: [], + successors: [], + anchor: { + _id: '', + fields: [], + sort: [], + }, + anchorStatus: LoadingStatus.UNINITIALIZED, + predecessorsStatus: LoadingStatus.UNINITIALIZED, + successorsStatus: LoadingStatus.UNINITIALIZED, ...getContextQueryDefaults(), }; if (typeof urlState !== 'object') { 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 4062edf3909b8..e259d433303d6 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,59 +6,39 @@ * Side Public License, v 1. */ -import React, { useState, Fragment, useEffect, useRef, useCallback } from 'react'; +import React, { useState, Fragment, useEffect, useRef, useMemo } 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 { FilterManager } from 'src/plugins/data/public'; -import { map } from 'rxjs/operators'; -import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; +import { cloneDeep } from 'lodash'; +import { + CONTEXT_DEFAULT_SIZE_SETTING, + DOC_HIDE_TIME_COLUMN_SETTING, + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IndexPattern } from '../../../../../data/common/index_patterns'; -import { LoadingState, LoadingStatus } from '../../angular/context_query_state'; +import { LoadingState, LoadingStatus, LoadingStatusEntry } 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'; -import { DocViewFilterFn } from '../../doc_views/doc_views_types'; -import { getServices, SortDirection } from '../../../kibana_services'; -import { GetStateReturn, AppState } from '../../angular/context_state'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { getServices } from '../../../kibana_services'; +import { AppState, isEqualFilters } from '../../angular/context_state'; import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; -import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; +import { EsHitRecord } 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'; -// import { fetchAnchorRow } from './context_app_actions'; +import { useContextAppActions } from './use_context_app_actions'; 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; - successorStatus: string; - defaultStepSize: number; - predecessorCount: number; - successorCount: number; - predecessorAvailable: number; - successorAvailable: number; - onChangePredecessorCount: (count: number) => void; - onChangeSuccessorCount: (count: number) => void; - useNewFieldsApi?: boolean; } const DataGridMemoized = React.memo(DiscoverGrid); @@ -69,40 +49,20 @@ function isLoading(status: LoadingState) { 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, - // hits: rows, - sorting, - anchorId, - filter, - minimumVisibleRows, - useNewFieldsApi, - } = renderProps; + const { uiSettings: config, capabilities, indexPatterns, navigation } = services; + const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), [ + config, + ]); + const { indexPattern, indexPatternId, anchorId } = renderProps; const [expandedDoc, setExpandedDoc] = useState(undefined); - // const isAnchorLoaded = anchor === LoadingStatus.LOADED; - // const isFailed = anchorStatus === LoadingStatus.FAILED; - // const allRowsLoaded = - // anchorStatus === LoadingStatus.LOADED && - // predecessorStatus === LoadingStatus.LOADED && - // successorStatus === LoadingStatus.LOADED; const isLegacy = config.get(DOC_TABLE_LEGACY); - // const anchorId = rows?.find(({ isAnchor }) => isAnchor)?._id; const { state, setAppState } = useContextAppState({ indexPattern, - indexPatternId, - anchorId, + defaultStepSize, services, }); const prevState = useRef(); @@ -114,60 +74,32 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { state.predecessorsStatus === LoadingStatus.LOADED && state.successorsStatus === LoadingStatus.LOADED; - // useEffect(() => { - // stateContainer.startSync(); - - // return () => stateContainer.stopSync(); - // }, [stateContainer]); - - const { context$, fetchContextRows, fetchAllRows } = useContextAppQuery({ + const { fetchSurroundingRows, fetchContextRows, fetchAllRows, addFilter } = useContextAppActions({ + anchorId, + indexPatternId, + state, + useNewFieldsApi, services, - useNewFieldsApi: !!useNewFieldsApi, + setAppState, }); /** - * Fetch docs + * Fetch docs on ui changes */ useEffect(() => { if (!prevState.current) { - fetchAllRows(state.predecessorCount, state.successorCount, { - indexPatternId, - anchorId, - tieBreakerField: state.queryParameters.tieBreakerField, - sort: state.sort, - }); - } else if ( - prevState.current.predecessorCount !== state.predecessorCount || - prevState.current.successorCount !== state.successorCount || - !isEqual(prevState.current.filters, state.filters) - ) { - fetchContextRows(state.predecessorCount, state.successorCount, { - indexPatternId, - anchor: state.anchor, - tieBreakerField: state.queryParameters.tieBreakerField, - sort: state.sort, - }); + fetchAllRows(); + } else if (prevState.current.predecessorCount !== state.predecessorCount) { + fetchSurroundingRows('predecessors'); + } else if (prevState.current.successorCount !== state.successorCount) { + fetchSurroundingRows('successors'); + } else if (!isEqualFilters(prevState.current.filters, state.filters)) { + fetchContextRows(state.anchor); } prevState.current = cloneDeep(state); - }, [state, indexPatternId, anchorId, fetchContextRows, fetchAllRows]); - - /** - * Sync app state with context$ - */ - useEffect(() => { - context$.subscribe((value) => { - setAppState(value); - }); + }, [state, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); - return () => context$.unsubscribe(); - }, [context$, setAppState]); - - const rows = [ - ...(state.predecessors || []), - ...(state.anchor ? [state.anchor] : []), - ...(state.successors || []), - ]; const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, config, @@ -177,23 +109,22 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { state, useNewFieldsApi: !!useNewFieldsApi, }); + const rows = [ + ...(state.predecessors || []), + ...(state.anchor ? [state.anchor] : []), + ...(state.successors || []), + ]; const actionBarProps = (type: string) => { - const { - defaultStepSize, - successorCount, - predecessorCount, - predecessorAvailable, - successorAvailable, - onChangePredecessorCount, - onChangeSuccessorCount, - } = renderProps; const isPredecessorType = type === PREDECESSOR_TYPE; return { defaultStepSize, - docCount: isPredecessorType ? predecessorCount : successorCount, - docCountAvailable: isPredecessorType ? predecessorAvailable : successorAvailable, - onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, + docCount: isPredecessorType ? state.predecessorCount : state.successorCount, + docCountAvailable: isPredecessorType ? state.predecessors.length : state.successors.length, + onChangeCount: (count) => { + const countKey = type === PREDECESSOR_TYPE ? 'predecessorCount' : 'successorCount'; + setAppState({ [countKey]: count }); + }, isLoading: isPredecessorType ? isLoading(state.predecessorsStatus) : isLoading(state.successorsStatus), @@ -206,19 +137,19 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return { ariaLabelledBy: 'surDocumentsAriaLabel', columns, - rows: rows.length !== 0 && rows, + rows: rows as ElasticSearchHit[], indexPattern, expandedDoc, isLoading: !allRowsLoaded, sampleSize: 0, - sort: sorting, + sort: state.sort, isSortEnabled: false, showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, services, useNewFieldsApi, isPaginationEnabled: false, setExpandedDoc, - onFilter: filter, + onFilter: addFilter, onAddColumn, onRemoveColumn, onSetColumns, @@ -230,17 +161,17 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return { columns, indexPattern, - minimumVisibleRows, + minimumVisibleRows: rows.length, rows, - onFilter: filter, + onFilter: addFilter, onAddColumn, onRemoveColumn, - sort: sorting.map((el) => [el]), + sort: state.sort.map((el) => [el]), useNewFieldsApi, } as DocTableLegacyProps; }; - const TopNavMenu = renderProps.topNavMenu; + const TopNavMenu = navigation.ui.TopNavMenu; const getNavBarProps = () => { return { appName: 'context', @@ -273,7 +204,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { {isFailed ? ( ) : ( 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 7ac8a7fd5e881..3764143de2078 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 @@ -11,28 +11,8 @@ import { ContextAppLegacy } from './context_app_legacy'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createContextAppLegacy(reactDirective: any) { return reactDirective(ContextAppLegacy, [ - ['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' }], - ['predecessorCount', { watchDepth: 'reference' }], - ['predecessorAvailable', { watchDepth: 'reference' }], - ['predecessorStatus', { watchDepth: 'reference' }], - ['onChangePredecessorCount', { watchDepth: 'reference' }], - ['successorCount', { watchDepth: 'reference' }], - ['successorAvailable', { watchDepth: 'reference' }], - ['successorStatus', { watchDepth: 'reference' }], - ['onChangeSuccessorCount', { watchDepth: 'reference' }], - ['useNewFieldsApi', { watchDepth: 'reference' }], - ['topNavMenu', { watchDepth: 'reference' }], ]); } diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx new file mode 100644 index 0000000000000..cb715de6744de --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx @@ -0,0 +1,150 @@ +/* + * 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 { DiscoverServices } from '../../../build_services'; +import { fetchAnchorProvider } from '../../angular/context/api/anchor'; +import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context'; +import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; +import { esFilters, IndexPatternField } from '../../../../../data/public'; +import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; +import { AppState, GetStateReturn } from '../../angular/context_state'; +import { popularizeField } from '../../helpers/popularize_field'; + +export function useContextAppActions({ + anchorId, + indexPatternId, + state, + useNewFieldsApi, + services, + setAppState, +}: { + anchorId: string; + indexPatternId: string; + state: AppState; + useNewFieldsApi: boolean; + services: DiscoverServices; + setAppState: GetStateReturn['setAppState']; +}) { + 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 fetchAnchorRow = useCallback(() => { + const { sort, tieBreakerField } = state; + + const [[, sortDir]] = sort; + + setAppState({ anchorStatus: LoadingStatus.LOADING }); + return fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) + .then((anchor) => { + setAppState({ anchor, anchorStatus: LoadingStatus.LOADED }); + return anchor; + }) + .catch((error) => { + setAppState({ + anchorStatus: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, + }); + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load the anchor document', + }), + text: toMountPoint({error.message}), + }); + throw error; + }); + }, [setAppState, fetchAnchor, indexPatternId, anchorId, toastNotifications, state]); + + const fetchSurroundingRows = useCallback( + (type: SurrDocType, fetchedAnchor?: EsHitRecord) => { + const filters = filterManager.getFilters(); + const { tieBreakerField, sort } = state; + const [[sortField, sortDir]] = sort; + + const count = type === 'predecessors' ? state.predecessorCount : state.successorCount; + const anchor = fetchedAnchor || state.anchor; + + setAppState({ + [`${type}Status`]: LoadingStatus.LOADING, + }); + + return fetchSurroundingDocs( + type, + indexPatternId, + anchor, + sortField, + tieBreakerField, + sortDir, + count, + filters + ) + .then((hits) => { + setAppState({ [type]: hits, [`${type}Status`]: LoadingStatus.LOADED }); + }) + .catch((error) => { + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadDocumentDescription', { + defaultMessage: 'Unable to load documents', + }), + text: toMountPoint({error.message}), + }); + throw error; + }); + }, + [fetchSurroundingDocs, filterManager, indexPatternId, setAppState, state, toastNotifications] + ); + + const fetchContextRows = useCallback( + (anchor) => { + return Promise.allSettled([ + fetchSurroundingRows('predecessors', anchor), + fetchSurroundingRows('successors', anchor), + ]); + }, + [fetchSurroundingRows] + ); + + const fetchAllRows = useCallback(() => fetchAnchorRow().then(fetchContextRows).catch(Error), [ + fetchAnchorRow, + fetchContextRows, + ]); + + const addFilter = useCallback( + async (field: IndexPatternField | string, values: unknown, operation: string) => { + const newFilters = esFilters.generateFilters( + filterManager, + field, + values, + operation, + indexPatternId + ); + filterManager.addFilters(newFilters); + if (indexPatterns) { + const indexPattern = await indexPatterns.get(indexPatternId); + const fieldName = typeof field === 'string' ? field : field.name; + await popularizeField(indexPattern, fieldName, indexPatterns); + } + }, + [filterManager, indexPatternId, indexPatterns] + ); + + return { fetchSurroundingRows, fetchContextRows, fetchAllRows, addFilter }; +} 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 deleted file mode 100644 index e1bd7fa4908fd..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_query.tsx +++ /dev/null @@ -1,184 +0,0 @@ -/* - * 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, useEffect, 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'; -import { Filter, SortDirection } from '../../../../../data/public'; -import { - ContextRows, - FailureReason, - LoadingState, - LoadingStatus, -} from '../../angular/context_query_state'; - -export type ContextAppMessage = Partial; - -interface ContextDocsMessage { - successors?: EsHitRecordList; - predecessors?: EsHitRecordList; - anchor?: EsHitRecord; - all?: EsHitRecordList; - anchorStatus?: LoadingState; - predecessorsStatus?: LoadingState; - successorsStatus?: LoadingState; -} - -interface FetchAllRowsParams { - anchorId: string; - indexPatternId: string; - tieBreakerField: string; - sort: [[string, SortDirection]]; -} -interface FetchSurrDocsParams { - indexPatternId: string; - anchor: EsHitRecord; - tieBreakerField: string; - sort: [[string, SortDirection]]; -} - -export function useContextAppQuery({ - services, - useNewFieldsApi, -}: { - services: DiscoverServices; - 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( - ({ - indexPatternId, - anchorId, - tieBreakerField, - sort, - }: { - indexPatternId: string; - anchorId: string; - tieBreakerField: string; - sort: [[string, SortDirection]]; - }) => { - const [[, sortDir]] = sort; - - context$.next({ anchorStatus: LoadingStatus.LOADING }); - return fetchAnchor(indexPatternId, anchorId, [ - fromPairs(sort), - { [tieBreakerField]: sortDir }, - ]) - .then( - (anchor: EsHitRecord): EsHitRecord => { - context$.next({ anchor, anchorStatus: LoadingStatus.LOADED }); - return anchor; - } - ) - .catch((error) => { - context$.next({ - anchorStatus: { reason: FailureReason.INVALID_TIEBREAKER }, - }); - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { - defaultMessage: 'Unable to load the anchor document', - }), - text: toMountPoint({error.message}), - }); - throw error; - }); - }, - [context$, fetchAnchor, toastNotifications] - ); - - const fetchSurroundingRows = useCallback( - (type, count, { indexPatternId, anchor, tieBreakerField, sort }: FetchSurrDocsParams) => { - const filters = filterManager.getFilters(); - const [[sortField, sortDir]] = sort; - - context$.next({ [`${type}Status`]: LoadingStatus.LOADING }); - return fetchSurroundingDocs( - type, - indexPatternId, - anchor, - sortField, - tieBreakerField, - sortDir, - count, - filters - ).then( - (hits: EsHitRecordList) => - context$.next({ [type]: hits, [`${type}Status`]: LoadingStatus.LOADED }), - (error) => { - context$.next({ [`${type}Status`]: { reason: LoadingStatus.FAILED } }); - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), - text: toMountPoint({error.message}), - }); - throw error; - } - ); - }, - [context$, fetchSurroundingDocs, filterManager, toastNotifications] - ); - - const fetchContextRows = useCallback( - (predecessorCount: number, successorCount: number, params: FetchSurrDocsParams) => { - return Promise.all([ - fetchSurroundingRows('predecessors', predecessorCount, params), - fetchSurroundingRows('successors', successorCount, params), - ]); - }, - [fetchSurroundingRows] - ); - - const fetchAllRows = useCallback( - ( - predecessorCount: number, - successorCount: number, - { anchorId, ...restParams }: FetchAllRowsParams - ) => { - fetchAnchorRow({ - anchorId, - ...restParams, - }).then((anchor: EsHitRecord) => { - return fetchContextRows(predecessorCount, successorCount, { - anchor, - ...restParams, - }); - }); - }, - [fetchAnchorRow, fetchContextRows] - ); - - return { context$, fetchContextRows, 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 index 65157565318c9..d178ff3abf6cb 100644 --- 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 @@ -11,29 +11,21 @@ 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 { 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, + defaultStepSize, services, }: { indexPattern: IndexPattern; - indexPatternId: string; - anchorId: string; + defaultStepSize: number; 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, @@ -41,16 +33,16 @@ export function useContextAppState({ 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) + getContextQueryDefaults: () => ({ + defaultStepSize, + tieBreakerField: getFirstSortableField( + indexPattern, + config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING) ), + useNewFieldsApi: !config.get(SEARCH_FIELDS_FROM_SOURCE), + }), }); - }, [config, history, indexPattern, anchorId, indexPatternId, core.notifications.toasts]); + }, [defaultStepSize, config, history, indexPattern, core.notifications.toasts]); const [state, setState] = useState(stateContainer.appState.getState()); @@ -58,7 +50,6 @@ export function useContextAppState({ * Sync app state */ useEffect(() => { - // take care of context state updates const unsubscribeAppState = stateContainer.appState.subscribe(async (newState) => { setState(newState); }); @@ -70,7 +61,6 @@ export function useContextAppState({ * 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)); From 640b75d596b89c13f7d6f95cd77d4742044d5427 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 1 Jun 2021 01:39:48 +0300 Subject: [PATCH 17/36] [Discover] split up context state and components --- .../public/application/angular/context.js | 15 +- .../angular/context_query_state.ts | 113 ++------ .../application/angular/context_state.ts | 39 +-- ....test.tsx => context_app_content.test.tsx} | 1 - .../context_app/context_app_content.tsx | 175 +++++++++++++ ...pp_legacy.scss => context_app_layout.scss} | 0 .../context_app/context_app_layout.tsx | 192 ++++++++++++++ .../context_app/context_app_legacy.tsx | 247 ------------------ .../context_app_legacy_directive.ts | 4 +- .../context_app/use_context_app_actions.tsx | 150 ----------- .../context_app/use_context_app_fetch.tsx | 200 ++++++++++++++ .../context_app/use_context_app_state.ts | 22 +- 12 files changed, 618 insertions(+), 540 deletions(-) rename src/plugins/discover/public/application/components/context_app/{context_app_legacy.test.tsx => context_app_content.test.tsx} (98%) create mode 100644 src/plugins/discover/public/application/components/context_app/context_app_content.tsx rename src/plugins/discover/public/application/components/context_app/{context_app_legacy.scss => context_app_layout.scss} (100%) create mode 100644 src/plugins/discover/public/application/components/context_app/context_app_layout.tsx delete mode 100644 src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx delete mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx create mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index e51863871b175..49846ecb9bc3c 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -30,10 +30,15 @@ const k7Breadcrumbs = ($route) => { }; getAngularModule().config(($routeProvider) => { - $routeProvider.when('/context/:indexPatternId/:id*', { - controller: ContextAppRouteController, + $routeProvider.when('/context/:indexPatternId/:id', { + controller: function ($routeParams, $scope, $route) { + this.indexPattern = $route.current.locals.indexPattern.ip; + this.anchorId = $routeParams.id; + this.indexPatternId = $route.current.params.indexPatternId; + }, k7Breadcrumbs, controllerAs: 'contextAppRoute', + reloadOnSearch: false, resolve: { indexPattern: ($route, Promise) => { const indexPattern = getServices().indexPatterns.get($route.current.params.indexPatternId); @@ -43,9 +48,3 @@ getAngularModule().config(($routeProvider) => { template: contextAppRouteTemplate, }); }); - -function ContextAppRouteController($routeParams, $scope, $route) { - this.indexPattern = $route.current.locals.indexPattern.ip; - this.anchorId = $routeParams.id; - this.indexPatternId = $route.current.params.indexPatternId; -} 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 aa5b46863460a..6693239c2a57d 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -6,33 +6,34 @@ * Side Public License, v 1. */ -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 ContextQueryState { - loadingStatus: LoadingStatusState; - queryParameters: QueryParameters; - rows: ContextRows; - hits: EsHitRecordList; +export interface ContextFetchState { + /** + * Documents listed before anchor + */ predecessors: EsHitRecordList; + /** + * Documents after anchor + */ successors: EsHitRecordList; + /** + * Anchor document + */ anchor: EsHitRecord; + /** + * Anchor fetch status + */ anchorStatus: LoadingState; + /** + * Predecessors fetch status + */ predecessorsStatus: LoadingState; + /** + * Successors fetch status + */ successorsStatus: LoadingState; - predecessorCount: number; - successorCount: number; - useNewFieldsApi: boolean; -} - -export interface ContextQueryParams { - defaultStepSize: number; - tieBreakerField: string; - useNewFieldsApi: boolean; } export enum LoadingStatus { @@ -41,81 +42,25 @@ export enum LoadingStatus { LOADING = 'loading', UNINITIALIZED = 'uninitialized', } + export enum FailureReason { UNKNOWN = 'unknown', INVALID_TIEBREAKER = 'invalid_tiebreaker', } + export type LoadingStatusEntry = Partial<{ status: LoadingStatus; reason: FailureReason; error: Error; }>; -export interface LoadingStatusState { - anchor: LoadingState; - predecessors: LoadingState; - successors: LoadingState; -} - export type LoadingState = LoadingStatus | LoadingStatusEntry; -export interface QueryParameters { - anchorId: string; - columns: string[]; - defaultStepSize: number; - filters: Filter[]; - indexPatternId: string; - predecessorCount: number; - successorCount: number; - sort: Array<[string, SortDirection]>; - tieBreakerField: string; -} - -export interface ContextRows { - all: EsHitRecordList; - anchor: EsHitRecord; - 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 - ), - hits: [], - predecessors: [], - successors: [], - anchor: { - fields: [], - sort: [], - _id: '', - }, - rows: { - all: [], - anchor: { - fields: [], - sort: [], - _id: '', - }, - predecessors: [], - successors: [], - }, - loadingStatus: createInitialLoadingStatusState(), - anchorStatus: LoadingStatus.UNINITIALIZED, - predecessorsStatus: LoadingStatus.UNINITIALIZED, - successorsStatus: LoadingStatus.UNINITIALIZED, - predecessorCount: 5, - successorCount: 5, - useNewFieldsApi, - }; -} +export const getInitialContextQueryState = (): ContextFetchState => ({ + anchor: {} as EsHitRecord, + predecessors: [], + successors: [], + anchorStatus: LoadingStatus.UNINITIALIZED, + predecessorsStatus: LoadingStatus.UNINITIALIZED, + successorsStatus: LoadingStatus.UNINITIALIZED, +}); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 3026c4b2a54d6..7c2e0f3a13399 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -18,10 +18,8 @@ import { } from '../../../../kibana_utils/public'; import { esFilters, FilterManager, Filter, SortDirection } from '../../../../data/public'; import { handleSourceColumnState } from './helpers'; -import { ContextQueryParams, LoadingState, LoadingStatus } from './context_query_state'; -import { EsHitRecord, EsHitRecordList } from './context/api/context'; -export interface AppState extends ContextQueryParams { +export interface AppState { /** * Columns displayed in the table, cannot be changed by UI, just in discover's main app */ @@ -42,15 +40,6 @@ export interface AppState extends ContextQueryParams { * Number of records to be fetched after the anchor records (older records) */ successorCount: number; - - hits: EsHitRecordList; - predecessors: EsHitRecordList; - successors: EsHitRecordList; - anchor: EsHitRecord; - - anchorStatus: LoadingState; - predecessorsStatus: LoadingState; - successorsStatus: LoadingState; } interface GlobalState { @@ -89,11 +78,6 @@ export interface GetStateParams { * core ui settings service */ uiSettings: IUiSettingsClient; - - /** - * Default state used for data querying - */ - getContextQueryDefaults: () => ContextQueryParams; } export interface GetStateReturn { @@ -145,7 +129,6 @@ export function getState({ history, toasts, uiSettings, - getContextQueryDefaults, }: GetStateParams): GetStateReturn { const stateStorage = createKbnUrlStateStorage({ useHash: storeInSessionStorage, @@ -161,8 +144,7 @@ export function getState({ defaultStepSize, timeFieldName, appStateFromUrl, - uiSettings, - getContextQueryDefaults + uiSettings ); const appStateContainer = createStateContainer(appStateInitial); @@ -287,27 +269,14 @@ function createInitialAppState( defaultSize: number, timeFieldName: string, urlState: AppState, - uiSettings: IUiSettingsClient, - getContextQueryDefaults: () => ContextQueryParams + uiSettings: IUiSettingsClient ): AppState { const defaultState: AppState = { columns: ['_source'], filters: [], predecessorCount: defaultSize, - sort: [[timeFieldName, SortDirection.desc]], successorCount: defaultSize, - hits: [], - predecessors: [], - successors: [], - anchor: { - _id: '', - fields: [], - sort: [], - }, - anchorStatus: LoadingStatus.UNINITIALIZED, - predecessorsStatus: LoadingStatus.UNINITIALIZED, - successorsStatus: LoadingStatus.UNINITIALIZED, - ...getContextQueryDefaults(), + sort: [[timeFieldName, SortDirection.desc]], }; if (typeof urlState !== 'object') { return defaultState; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx similarity index 98% rename from src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx rename to src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx index 09ad7e952cf0f..54c9d43167768 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx @@ -10,7 +10,6 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; import { IndexPattern } from '../../../../../data/common/index_patterns'; -import { ContextAppLegacy } from './context_app_legacy'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx new file mode 100644 index 0000000000000..b201b1fde8231 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -0,0 +1,175 @@ +/* + * 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, { useState, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; +import { + DocTableLegacy, + DocTableLegacyProps, +} from '../../angular/doc_table/create_doc_table_react'; +import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { LoadingState, LoadingStatus } from '../../angular/context_query_state'; +import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; +import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; +import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { AppState } from '../../angular/context_state'; +import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; +import { DiscoverServices } from '../../../build_services'; +import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; + +export interface ContextAppContentProps { + columns: string[]; + onAddColumn: (columnsName: string) => void; + onRemoveColumn: (columnsName: string) => void; + onSetColumns: (columnsNames: string[]) => void; + services: DiscoverServices; + indexPattern: IndexPattern; + predecessorCount: number; + successorCount: number; + rows: EsHitRecordList; + sort: SortPairArr[]; + predecessors: EsHitRecordList; + successors: EsHitRecordList; + anchorStatus: LoadingState; + predecessorsStatus: LoadingState; + successorsStatus: LoadingState; + useNewFieldsApi: boolean; + defaultStepSize: number; + isLegacy: boolean; + setAppState: (newState: Partial) => void; + addFilter: ( + field: IndexPatternField | string, + values: unknown, + operation: string + ) => Promise; +} + +const DataGridMemoized = React.memo(DiscoverGrid); +const PREDECESSOR_TYPE = 'predecessors'; +const SUCCESSOR_TYPE = 'successors'; + +function isLoading(status: LoadingState) { + return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; +} + +export function ContextAppContent({ + columns, + onAddColumn, + onRemoveColumn, + onSetColumns, + services, + indexPattern, + predecessorCount, + successorCount, + rows, + sort, + predecessors, + successors, + anchorStatus, + predecessorsStatus, + successorsStatus, + defaultStepSize, + useNewFieldsApi, + isLegacy, + setAppState, + addFilter, +}: ContextAppContentProps) { + const { uiSettings: config } = services; + const [expandedDoc, setExpandedDoc] = useState(undefined); + const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + + const actionBarProps = (type: string) => { + const isPredecessorType = type === PREDECESSOR_TYPE; + return { + defaultStepSize, + docCount: isPredecessorType ? predecessorCount : successorCount || defaultStepSize, + docCountAvailable: isPredecessorType ? predecessors.length : successors.length, + onChangeCount: (count) => { + const countKey = type === PREDECESSOR_TYPE ? 'predecessorCount' : 'successorCount'; + setAppState({ [countKey]: count }); + }, + isLoading: isPredecessorType + ? isLoading(predecessorsStatus || LoadingStatus.UNINITIALIZED) + : isLoading(successorsStatus || LoadingStatus.UNINITIALIZED), + type, + isDisabled: !isAnchorLoaded, + } as ActionBarProps; + }; + + const docTableProps = () => { + return { + ariaLabelledBy: 'surDocumentsAriaLabel', + columns, + rows: rows as ElasticSearchHit[], + indexPattern, + expandedDoc, + isLoading: isLoading(anchorStatus || LoadingStatus.UNINITIALIZED), + sampleSize: 0, + sort, + isSortEnabled: false, + showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + services, + useNewFieldsApi, + isPaginationEnabled: false, + setExpandedDoc, + onFilter: addFilter, + onAddColumn, + onRemoveColumn, + onSetColumns, + } as DiscoverGridProps; + }; + + const legacyDocTableProps = () => { + // @ts-expect-error doesn't implement full DocTableLegacyProps interface + return { + columns, + indexPattern, + minimumVisibleRows: rows.length, + rows, + onFilter: addFilter, + onAddColumn, + onRemoveColumn, + sort: sort.map((el) => [el]), + useNewFieldsApi, + } as DocTableLegacyProps; + }; + + const loadingFeedback = () => { + if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { + return ( + + + + ); + } + return null; + }; + + return ( + + + {loadingFeedback()} + + {isLegacy ? ( + isAnchorLoaded && ( +
+ +
+ ) + ) : ( +
+ +
+ )} + + +
+ ); +} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_layout.scss similarity index 100% rename from src/plugins/discover/public/application/components/context_app/context_app_legacy.scss rename to src/plugins/discover/public/application/components/context_app/context_app_layout.scss diff --git a/src/plugins/discover/public/application/components/context_app/context_app_layout.tsx b/src/plugins/discover/public/application/components/context_app/context_app_layout.tsx new file mode 100644 index 0000000000000..6d42d18c8c4a0 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_layout.tsx @@ -0,0 +1,192 @@ +/* + * 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, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react'; +import classNames from 'classnames'; +import './context_app_layout.scss'; +import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; +import './context_app_layout.scss'; +import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; +import { cloneDeep } from 'lodash'; +import { esFilters } from '../../../../../data/public'; +import { + CONTEXT_DEFAULT_SIZE_SETTING, + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../../common'; +import { ContextErrorMessage } from '../context_error_message'; +import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { LoadingStatus, LoadingStatusEntry } from '../../angular/context_query_state'; +import { getServices } from '../../../kibana_services'; +import { AppState, isEqualFilters } from '../../angular/context_state'; +import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; +import { useContextAppState } from './use_context_app_state'; +import { useContextAppFetch } from './use_context_app_fetch'; +import { popularizeField } from '../../helpers/popularize_field'; +import { ContextAppContent } from './context_app_content'; + +const ContextAppContentMemoized = memo(ContextAppContent); + +export interface ContextAppLayoutProps { + indexPattern: IndexPattern; + indexPatternId: string; + anchorId: string; +} + +export const ContextAppLayout = ({ + indexPattern, + indexPatternId, + anchorId, +}: ContextAppLayoutProps) => { + const services = getServices(); + const { uiSettings: config, capabilities, indexPatterns, navigation, filterManager } = services; + + const isLegacy = useMemo(() => config.get(DOC_TABLE_LEGACY), [config]); + const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), [ + config, + ]); + + /** + * Context app state + */ + const { appState, setAppState } = useContextAppState({ + indexPattern, + defaultStepSize, + services, + }); + const prevState = useRef(); + + /** + * Context fetched state + */ + const { fetchedState, fetchMoreRows, fetchContextRows, fetchAllRows } = useContextAppFetch({ + anchorId, + indexPatternId, + indexPattern, + appState, + useNewFieldsApi, + services, + }); + + /** + * Fetch docs on ui changes + */ + useEffect(() => { + if (!prevState.current) { + fetchAllRows(); + } else if (prevState.current.predecessorCount !== appState.predecessorCount) { + fetchMoreRows('predecessors'); + } else if (prevState.current.successorCount !== appState.successorCount) { + fetchMoreRows('successors'); + } else if (!isEqualFilters(prevState.current.filters, appState.filters)) { + fetchContextRows(); + } + + prevState.current = cloneDeep(appState); + }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchMoreRows]); + + const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + state: appState, + useNewFieldsApi, + setAppState, + }); + const rows = [ + ...(fetchedState.predecessors || []), + ...(fetchedState.anchor._id ? [fetchedState.anchor] : []), + ...(fetchedState.successors || []), + ]; + + const addFilter = useCallback( + async (field: IndexPatternField | string, values: unknown, operation: string) => { + const newFilters = esFilters.generateFilters( + filterManager, + field, + values, + operation, + indexPatternId + ); + filterManager.addFilters(newFilters); + if (indexPatterns) { + const fieldName = typeof field === 'string' ? field : field.name; + await popularizeField(indexPattern, fieldName, indexPatterns); + } + }, + [filterManager, indexPatternId, indexPatterns, indexPattern] + ); + + const TopNavMenu = navigation.ui.TopNavMenu; + const getNavBarProps = () => { + return { + appName: 'context', + showSearchBar: true, + showQueryBar: false, + showFilterBar: true, + showSaveQuery: false, + showDatePicker: false, + indexPatterns: [indexPattern], + useDefaultBehaviors: true, + }; + }; + + return ( + + {fetchedState.anchorStatus === LoadingStatus.FAILED ? ( + + ) : ( + + + + + + + + + + + + + + + + )} + + ); +}; 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 deleted file mode 100644 index e259d433303d6..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ /dev/null @@ -1,247 +0,0 @@ -/* - * 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, { useState, Fragment, useEffect, useRef, useMemo } 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 } from 'lodash'; -import { - CONTEXT_DEFAULT_SIZE_SETTING, - DOC_HIDE_TIME_COLUMN_SETTING, - DOC_TABLE_LEGACY, - SEARCH_FIELDS_FROM_SOURCE, -} from '../../../../common'; -import { ContextErrorMessage } from '../context_error_message'; -import { - DocTableLegacy, - DocTableLegacyProps, -} from '../../angular/doc_table/create_doc_table_react'; -import { IndexPattern } from '../../../../../data/common/index_patterns'; -import { LoadingState, LoadingStatus, LoadingStatusEntry } from '../../angular/context_query_state'; -import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; -import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; -import { ElasticSearchHit } from '../../doc_views/doc_views_types'; -import { getServices } from '../../../kibana_services'; -import { AppState, isEqualFilters } from '../../angular/context_state'; -import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; -import { EsHitRecord } from '../../angular/context/api/context'; -import { useContextAppState } from './use_context_app_state'; -import { useContextAppActions } from './use_context_app_actions'; - -export interface ContextAppProps { - indexPattern: IndexPattern; - indexPatternId: string; - anchorId: string; -} - -const DataGridMemoized = React.memo(DiscoverGrid); -const PREDECESSOR_TYPE = 'predecessors'; -const SUCCESSOR_TYPE = 'successors'; - -function isLoading(status: LoadingState) { - return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; -} - -export function ContextAppLegacy(renderProps: ContextAppProps) { - const services = getServices(); - const { uiSettings: config, capabilities, indexPatterns, navigation } = services; - const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); - const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), [ - config, - ]); - const { indexPattern, indexPatternId, anchorId } = renderProps; - const [expandedDoc, setExpandedDoc] = useState(undefined); - const isLegacy = config.get(DOC_TABLE_LEGACY); - - const { state, setAppState } = useContextAppState({ - indexPattern, - defaultStepSize, - services, - }); - const prevState = useRef(); - - const isAnchorLoaded = state.anchorStatus === LoadingStatus.LOADED; - const isFailed = state.anchorStatus === LoadingStatus.FAILED; - const allRowsLoaded = - state.anchorStatus === LoadingStatus.LOADED && - state.predecessorsStatus === LoadingStatus.LOADED && - state.successorsStatus === LoadingStatus.LOADED; - - const { fetchSurroundingRows, fetchContextRows, fetchAllRows, addFilter } = useContextAppActions({ - anchorId, - indexPatternId, - state, - useNewFieldsApi, - services, - setAppState, - }); - - /** - * Fetch docs on ui changes - */ - useEffect(() => { - if (!prevState.current) { - fetchAllRows(); - } else if (prevState.current.predecessorCount !== state.predecessorCount) { - fetchSurroundingRows('predecessors'); - } else if (prevState.current.successorCount !== state.successorCount) { - fetchSurroundingRows('successors'); - } else if (!isEqualFilters(prevState.current.filters, state.filters)) { - fetchContextRows(state.anchor); - } - - prevState.current = cloneDeep(state); - }, [state, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); - - const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ - capabilities, - config, - indexPattern, - indexPatterns, - setAppState, - state, - useNewFieldsApi: !!useNewFieldsApi, - }); - const rows = [ - ...(state.predecessors || []), - ...(state.anchor ? [state.anchor] : []), - ...(state.successors || []), - ]; - - const actionBarProps = (type: string) => { - const isPredecessorType = type === PREDECESSOR_TYPE; - return { - defaultStepSize, - docCount: isPredecessorType ? state.predecessorCount : state.successorCount, - docCountAvailable: isPredecessorType ? state.predecessors.length : state.successors.length, - onChangeCount: (count) => { - const countKey = type === PREDECESSOR_TYPE ? 'predecessorCount' : 'successorCount'; - setAppState({ [countKey]: count }); - }, - isLoading: isPredecessorType - ? isLoading(state.predecessorsStatus) - : isLoading(state.successorsStatus), - type, - isDisabled: !isAnchorLoaded, - } as ActionBarProps; - }; - - const docTableProps = () => { - return { - ariaLabelledBy: 'surDocumentsAriaLabel', - columns, - rows: rows as ElasticSearchHit[], - indexPattern, - expandedDoc, - isLoading: !allRowsLoaded, - sampleSize: 0, - sort: state.sort, - isSortEnabled: false, - showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, - services, - useNewFieldsApi, - isPaginationEnabled: false, - setExpandedDoc, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - onSetColumns, - } as DiscoverGridProps; - }; - - const legacyDocTableProps = () => { - // @ts-expect-error doesn't implement full DocTableLegacyProps interface - return { - columns, - indexPattern, - minimumVisibleRows: rows.length, - rows, - onFilter: addFilter, - onAddColumn, - onRemoveColumn, - sort: state.sort.map((el) => [el]), - useNewFieldsApi, - } as DocTableLegacyProps; - }; - - const TopNavMenu = navigation.ui.TopNavMenu; - const getNavBarProps = () => { - return { - appName: 'context', - showSearchBar: true, - showQueryBar: false, - showFilterBar: true, - showSaveQuery: false, - showDatePicker: false, - indexPatterns: [renderProps.indexPattern], - useDefaultBehaviors: true, - }; - }; - - const loadingFeedback = () => { - if ( - state.anchorStatus === LoadingStatus.UNINITIALIZED || - state.anchorStatus === LoadingStatus.LOADING - ) { - return ( - - - - ); - } - return null; - }; - - return ( - - {isFailed ? ( - - ) : ( - - - - - - - - - - - - - {loadingFeedback()} - - {isLegacy ? ( - isAnchorLoaded && ( -
- -
- ) - ) : ( -
- -
- )} - - -
-
-
- )} -
- ); -} 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 3764143de2078..838f3faca69ec 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 @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { ContextAppLegacy } from './context_app_legacy'; +import { ContextAppLayout } from './context_app_layout'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createContextAppLegacy(reactDirective: any) { - return reactDirective(ContextAppLegacy, [ + return reactDirective(ContextAppLayout, [ ['indexPattern', { watchDepth: 'reference' }], ['indexPatternId', { watchDepth: 'reference' }], ['anchorId', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx deleted file mode 100644 index cb715de6744de..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_actions.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * 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 { DiscoverServices } from '../../../build_services'; -import { fetchAnchorProvider } from '../../angular/context/api/anchor'; -import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context'; -import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; -import { esFilters, IndexPatternField } from '../../../../../data/public'; -import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; -import { AppState, GetStateReturn } from '../../angular/context_state'; -import { popularizeField } from '../../helpers/popularize_field'; - -export function useContextAppActions({ - anchorId, - indexPatternId, - state, - useNewFieldsApi, - services, - setAppState, -}: { - anchorId: string; - indexPatternId: string; - state: AppState; - useNewFieldsApi: boolean; - services: DiscoverServices; - setAppState: GetStateReturn['setAppState']; -}) { - 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 fetchAnchorRow = useCallback(() => { - const { sort, tieBreakerField } = state; - - const [[, sortDir]] = sort; - - setAppState({ anchorStatus: LoadingStatus.LOADING }); - return fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) - .then((anchor) => { - setAppState({ anchor, anchorStatus: LoadingStatus.LOADED }); - return anchor; - }) - .catch((error) => { - setAppState({ - anchorStatus: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, - }); - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { - defaultMessage: 'Unable to load the anchor document', - }), - text: toMountPoint({error.message}), - }); - throw error; - }); - }, [setAppState, fetchAnchor, indexPatternId, anchorId, toastNotifications, state]); - - const fetchSurroundingRows = useCallback( - (type: SurrDocType, fetchedAnchor?: EsHitRecord) => { - const filters = filterManager.getFilters(); - const { tieBreakerField, sort } = state; - const [[sortField, sortDir]] = sort; - - const count = type === 'predecessors' ? state.predecessorCount : state.successorCount; - const anchor = fetchedAnchor || state.anchor; - - setAppState({ - [`${type}Status`]: LoadingStatus.LOADING, - }); - - return fetchSurroundingDocs( - type, - indexPatternId, - anchor, - sortField, - tieBreakerField, - sortDir, - count, - filters - ) - .then((hits) => { - setAppState({ [type]: hits, [`${type}Status`]: LoadingStatus.LOADED }); - }) - .catch((error) => { - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), - text: toMountPoint({error.message}), - }); - throw error; - }); - }, - [fetchSurroundingDocs, filterManager, indexPatternId, setAppState, state, toastNotifications] - ); - - const fetchContextRows = useCallback( - (anchor) => { - return Promise.allSettled([ - fetchSurroundingRows('predecessors', anchor), - fetchSurroundingRows('successors', anchor), - ]); - }, - [fetchSurroundingRows] - ); - - const fetchAllRows = useCallback(() => fetchAnchorRow().then(fetchContextRows).catch(Error), [ - fetchAnchorRow, - fetchContextRows, - ]); - - const addFilter = useCallback( - async (field: IndexPatternField | string, values: unknown, operation: string) => { - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - indexPatternId - ); - filterManager.addFilters(newFilters); - if (indexPatterns) { - const indexPattern = await indexPatterns.get(indexPatternId); - const fieldName = typeof field === 'string' ? field : field.name; - await popularizeField(indexPattern, fieldName, indexPatterns); - } - }, - [filterManager, indexPatternId, indexPatterns] - ); - - return { fetchSurroundingRows, fetchContextRows, fetchAllRows, addFilter }; -} diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx new file mode 100644 index 0000000000000..2257bc8b3abaf --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -0,0 +1,200 @@ +/* + * 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, useState } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { fromPairs } from 'lodash'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; +import { fetchAnchorProvider } from '../../angular/context/api/anchor'; +import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context'; +import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; +import { IndexPattern } from '../../../../../data/public'; +import { + ContextFetchState, + FailureReason, + getInitialContextQueryState, + LoadingStatus, +} from '../../angular/context_query_state'; +import { AppState } from '../../angular/context_state'; +import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; + +const createError = (statusKey: string, error: Error) => ({ + [statusKey]: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, +}); + +export function useContextAppFetch({ + anchorId, + indexPatternId, + indexPattern, + appState, + useNewFieldsApi, + services, +}: { + anchorId: string; + indexPatternId: string; + indexPattern: IndexPattern; + appState: AppState; + useNewFieldsApi: boolean; + services: DiscoverServices; +}) { + const { uiSettings: config, data, indexPatterns, toastNotifications, filterManager } = services; + + const searchSource = useMemo(() => { + return data.search.searchSource.createEmpty(); + }, [data.search.searchSource]); + const tieBreakerField = useMemo( + () => getFirstSortableField(indexPattern, config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)), + [config, indexPattern] + ); + const fetchAnchor = useMemo(() => { + return fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi); + }, [indexPatterns, searchSource, useNewFieldsApi]); + const { fetchSurroundingDocs } = useMemo( + () => fetchContextProvider(indexPatterns, useNewFieldsApi), + [indexPatterns, useNewFieldsApi] + ); + + const [fetchedState, setFetchedState] = useState( + getInitialContextQueryState() + ); + + const setState = useCallback((values: Partial) => { + setFetchedState((prevState) => ({ ...prevState, ...values })); + }, []); + + const fetchAnchorRow = useCallback(async () => { + const { sort } = appState; + const [[, sortDir]] = sort; + + try { + const anchor = await fetchAnchor(indexPatternId, anchorId, [ + fromPairs(sort), + { [tieBreakerField]: sortDir }, + ]); + setState({ anchor, anchorStatus: LoadingStatus.LOADED }); + return anchor; + } catch (error) { + setState({ + anchorStatus: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, + }); + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load the anchor document', + }), + text: toMountPoint({error.message}), + }); + } + }, [ + appState, + fetchAnchor, + indexPatternId, + anchorId, + tieBreakerField, + setState, + toastNotifications, + ]); + + const fetchSurroundingRows = useCallback( + async (type: SurrDocType, fetchedAnchor?: EsHitRecord) => { + const filters = filterManager.getFilters(); + const { sort } = appState; + const [[sortField, sortDir]] = sort; + + const count = type === 'predecessors' ? appState.predecessorCount : appState.successorCount; + const anchor = fetchedAnchor || fetchedState.anchor; + + try { + return await fetchSurroundingDocs( + type, + indexPatternId, + anchor as EsHitRecord, + sortField, + tieBreakerField, + sortDir, + count, + filters + ); + } catch (error) { + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadDocumentDescription', { + defaultMessage: 'Unable to load documents', + }), + text: toMountPoint({error.message}), + }); + } + }, + [ + filterManager, + appState, + fetchedState.anchor, + fetchSurroundingDocs, + indexPatternId, + tieBreakerField, + toastNotifications, + ] + ); + + const fetchContextRows = useCallback( + async (anchor?: EsHitRecord) => { + setState({ + predecessorsStatus: LoadingStatus.LOADING, + successorsStatus: LoadingStatus.LOADING, + }); + + const [predecessors, successors] = await Promise.allSettled([ + fetchSurroundingRows('predecessors', anchor), + fetchSurroundingRows('successors', anchor), + ]); + + const predecessorsStatus = + predecessors.status === 'fulfilled' + ? LoadingStatus.LOADED + : createError('predecessorsStatus', predecessors.reason); + const successorsStatus = + successors.status === 'fulfilled' + ? LoadingStatus.LOADED + : createError('successorsStatus', successors.reason); + + setState({ + predecessorsStatus, + successorsStatus, + successors: successors.status === 'fulfilled' ? successors.value : [], + predecessors: predecessors.status === 'fulfilled' ? predecessors.value : [], + }); + }, + [fetchSurroundingRows, setState] + ); + + const fetchAllRows = useCallback( + () => fetchAnchorRow().then((anchor) => anchor && fetchContextRows(anchor)), + [fetchAnchorRow, fetchContextRows] + ); + + const fetchMoreRows = useCallback( + async (type) => { + const statusKey = `${type}Status`; + setState({ [statusKey]: LoadingStatus.LOADING }); + + try { + const rows = await fetchSurroundingRows(type); + setState({ [type]: rows, [statusKey]: LoadingStatus.LOADED }); + } catch (error) { + setState(createError(statusKey, error)); + } + }, + [fetchSurroundingRows, setState] + ); + + return { + fetchedState, + fetchMoreRows, + fetchContextRows, + 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 index d178ff3abf6cb..cee11ff75adce 100644 --- 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 @@ -11,8 +11,6 @@ import { cloneDeep } from 'lodash'; import { IndexPattern } from '../../../../../data/public'; import { DiscoverServices } from '../../../build_services'; import { AppState, getState } from '../../angular/context_state'; -import { CONTEXT_TIE_BREAKER_FIELDS_SETTING, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; -import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; export function useContextAppState({ indexPattern, @@ -33,22 +31,20 @@ export function useContextAppState({ history: history(), toasts: core.notifications.toasts, uiSettings: config, - getContextQueryDefaults: () => ({ - defaultStepSize, - tieBreakerField: getFirstSortableField( - indexPattern, - config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING) - ), - useNewFieldsApi: !config.get(SEARCH_FIELDS_FROM_SOURCE), - }), }); }, [defaultStepSize, config, history, indexPattern, core.notifications.toasts]); - const [state, setState] = useState(stateContainer.appState.getState()); + const [appState, setState] = useState(stateContainer.appState.getState()); /** - * Sync app state + * Sync with app state container */ + useEffect(() => { + stateContainer.startSync(); + + return () => stateContainer.stopSync(); + }, [stateContainer]); + useEffect(() => { const unsubscribeAppState = stateContainer.appState.subscribe(async (newState) => { setState(newState); @@ -75,7 +71,7 @@ export function useContextAppState({ }, [filterManager, stateContainer]); return { - state, + appState, stateContainer, setAppState: stateContainer.setAppState, }; From 33ae8a54110503e538bc45de3d1be80c6a4b0201 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 2 Jun 2021 23:25:52 +0300 Subject: [PATCH 18/36] [Discover] revert redundant css class --- .../application/components/discover_grid/discover_grid.scss | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index 81102641fe2e3..053b405b90acb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -81,10 +81,6 @@ @include euiTextTruncate; } -.dscDiscoverGrid__cell--highlight { - background-color: tintOrShade($euiColorPrimary, 90%, 70%); -} - .dscDiscoverGrid__descriptionListDescription { word-break: normal !important; } From ffdf1ec6885495ea91f3eca74a4a6616ca9926f8 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 3 Jun 2021 09:41:47 +0300 Subject: [PATCH 19/36] [Discover] rename component --- .../{context_app_layout.scss => context_app.scss} | 0 .../{context_app_layout.tsx => context_app.tsx} | 11 +++-------- .../context_app/context_app_legacy_directive.ts | 4 ++-- .../components/context_app/use_context_app_fetch.tsx | 8 ++++---- .../components/context_app/use_context_app_state.ts | 2 +- 5 files changed, 10 insertions(+), 15 deletions(-) rename src/plugins/discover/public/application/components/context_app/{context_app_layout.scss => context_app.scss} (100%) rename src/plugins/discover/public/application/components/context_app/{context_app_layout.tsx => context_app.tsx} (96%) diff --git a/src/plugins/discover/public/application/components/context_app/context_app_layout.scss b/src/plugins/discover/public/application/components/context_app/context_app.scss similarity index 100% rename from src/plugins/discover/public/application/components/context_app/context_app_layout.scss rename to src/plugins/discover/public/application/components/context_app/context_app.scss diff --git a/src/plugins/discover/public/application/components/context_app/context_app_layout.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx similarity index 96% rename from src/plugins/discover/public/application/components/context_app/context_app_layout.tsx rename to src/plugins/discover/public/application/components/context_app/context_app.tsx index 6d42d18c8c4a0..046595a42c5ef 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_layout.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -8,9 +8,8 @@ import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react'; import classNames from 'classnames'; -import './context_app_layout.scss'; +import './context_app.scss'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import './context_app_layout.scss'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { esFilters } from '../../../../../data/public'; @@ -32,17 +31,13 @@ import { ContextAppContent } from './context_app_content'; const ContextAppContentMemoized = memo(ContextAppContent); -export interface ContextAppLayoutProps { +export interface ContextAppProps { indexPattern: IndexPattern; indexPatternId: string; anchorId: string; } -export const ContextAppLayout = ({ - indexPattern, - indexPatternId, - anchorId, -}: ContextAppLayoutProps) => { +export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAppProps) => { const services = getServices(); const { uiSettings: config, capabilities, indexPatterns, navigation, filterManager } = services; 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 838f3faca69ec..596f105aaa8b5 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 @@ -6,11 +6,11 @@ * Side Public License, v 1. */ -import { ContextAppLayout } from './context_app_layout'; +import { ContextApp } from './context_app'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function createContextAppLegacy(reactDirective: any) { - return reactDirective(ContextAppLayout, [ + return reactDirective(ContextApp, [ ['indexPattern', { watchDepth: 'reference' }], ['indexPatternId', { watchDepth: 'reference' }], ['anchorId', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 2257bc8b3abaf..3307199e27353 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -24,7 +24,7 @@ import { import { AppState } from '../../angular/context_state'; import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; -const createError = (statusKey: string, error: Error) => ({ +const createUnknownError = (statusKey: string, error: Error) => ({ [statusKey]: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, }); @@ -155,11 +155,11 @@ export function useContextAppFetch({ const predecessorsStatus = predecessors.status === 'fulfilled' ? LoadingStatus.LOADED - : createError('predecessorsStatus', predecessors.reason); + : createUnknownError('predecessorsStatus', predecessors.reason); const successorsStatus = successors.status === 'fulfilled' ? LoadingStatus.LOADED - : createError('successorsStatus', successors.reason); + : createUnknownError('successorsStatus', successors.reason); setState({ predecessorsStatus, @@ -185,7 +185,7 @@ export function useContextAppFetch({ const rows = await fetchSurroundingRows(type); setState({ [type]: rows, [statusKey]: LoadingStatus.LOADED }); } catch (error) { - setState(createError(statusKey, error)); + setState(createUnknownError(statusKey, error)); } }, [fetchSurroundingRows, setState] 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 index cee11ff75adce..752ab665579d8 100644 --- 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 @@ -51,7 +51,7 @@ export function useContextAppState({ }); return () => unsubscribeAppState(); - }, [stateContainer, setState, filterManager]); + }, [stateContainer, setState]); /** * Take care of filters From ceec6be5981aff9f3fe74bf3d19c3c144994beb1 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 3 Jun 2021 16:51:25 +0300 Subject: [PATCH 20/36] [Discover] revert to upstream changes --- .../public/application/components/context_app/context_app.tsx | 2 +- .../application/components/context_app/context_app_content.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 046595a42c5ef..591a6bdf3c239 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -150,7 +150,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index b201b1fde8231..50772ab32d7a6 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -155,7 +155,7 @@ export function ContextAppContent({ return ( - {loadingFeedback()} + {isLegacy && loadingFeedback()} {isLegacy ? ( isAnchorLoaded && ( From 133c7978eb913c34855732a5f1ed3af82bcffca5 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 3 Jun 2021 21:58:01 +0300 Subject: [PATCH 21/36] [Discover] return upstream changes --- .../components/context_app/context_app.scss | 16 ---------------- ..._app_legacy.scss => context_app_content.scss} | 0 .../context_app/context_app_content.tsx | 2 ++ 3 files changed, 2 insertions(+), 16 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/context_app/context_app.scss rename src/plugins/discover/public/application/components/context_app/{context_app_legacy.scss => context_app_content.scss} (100%) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.scss b/src/plugins/discover/public/application/components/context_app/context_app.scss deleted file mode 100644 index 9f9c383c70d45..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/context_app.scss +++ /dev/null @@ -1,16 +0,0 @@ -@import '../../../../../../core/public/mixins'; - -.dscDocsPage { - @include kibanaFullBodyHeight(54px); // action bar height -} - -.dscDocsContent { - display: flex; - flex-direction: column; - height: 100%; -} - -.dscDocsGrid { - flex: 1 1 100%; - overflow: auto; -} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_content.scss similarity index 100% rename from src/plugins/discover/public/application/components/context_app/context_app_legacy.scss rename to src/plugins/discover/public/application/components/context_app/context_app_content.scss diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 50772ab32d7a6..446fdd4a3ffeb 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -7,6 +7,7 @@ */ import React, { useState, Fragment } from 'react'; +import './context_app_content.scss'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; @@ -118,6 +119,7 @@ export function ContextAppContent({ services, useNewFieldsApi, isPaginationEnabled: false, + controlColumnIds: ['openDetails'], setExpandedDoc, onFilter: addFilter, onAddColumn, From a19bd7c0fc756891626758b524d4529e051bf005 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Fri, 4 Jun 2021 21:33:26 +0300 Subject: [PATCH 22/36] [Discover] refactoring, context test update --- .../discover/public/__mocks__/ui_settings.ts | 12 ++- .../components/action_bar/action_bar.tsx | 8 +- .../angular/context_query_state.ts | 29 +++--- .../context_app/__mocks__/top_nav_menu.tsx | 2 +- .../context_app/context_app.test.tsx | 86 ++++++++++++++++ .../components/context_app/context_app.tsx | 40 ++++---- .../context_app/context_app_content.test.tsx | 93 +++++++---------- .../context_app/context_app_content.tsx | 98 ++++++++++-------- .../context_app/use_context_app_fetch.tsx | 99 +++++++++---------- .../context_error_message.tsx | 18 ++-- 10 files changed, 285 insertions(+), 200 deletions(-) create mode 100644 src/plugins/discover/public/application/components/context_app/context_app.test.tsx diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index 8347ff18edd7d..789eff9635efd 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,7 +7,13 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; +import { + CONTEXT_DEFAULT_SIZE_SETTING, + DEFAULT_COLUMNS_SETTING, + DOC_TABLE_LEGACY, + SAMPLE_SIZE_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -17,6 +23,10 @@ export const uiSettingsMock = ({ return ['default_column']; } else if (key === DOC_TABLE_LEGACY) { return true; + } else if (key === SEARCH_FIELDS_FROM_SOURCE) { + return false; + } else if (key === CONTEXT_DEFAULT_SIZE_SETTING) { + return 5; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index 3a64254db59c3..93dc4561b4f25 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -47,7 +47,7 @@ export interface ActionBarProps { * is triggered when the input containing count is changed * @param count */ - onChangeCount: (count: number) => void; + onChangeCount: (type: string, count: number) => void; /** * can be `predecessors` or `successors`, usage in context: * predecessors action bar + records (these are newer records) @@ -73,7 +73,7 @@ export function ActionBar({ const onSubmit = (ev: React.FormEvent) => { ev.preventDefault(); if (newDocCount !== docCount && isValid(newDocCount)) { - onChangeCount(newDocCount); + onChangeCount(type, newDocCount); } }; useEffect(() => { @@ -100,7 +100,7 @@ export function ActionBar({ const value = newDocCount + defaultStepSize; if (isValid(value)) { setNewDocCount(value); - onChangeCount(value); + onChangeCount(type, value); } }} flush="right" @@ -131,7 +131,7 @@ export function ActionBar({ }} onBlur={() => { if (newDocCount !== docCount && isValid(newDocCount)) { - onChangeCount(newDocCount); + onChangeCount(type, newDocCount); } }} type="number" 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 6693239c2a57d..4033ea384688c 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -25,15 +25,15 @@ export interface ContextFetchState { /** * Anchor fetch status */ - anchorStatus: LoadingState; + anchorStatus: LoadingStatusEntry; /** * Predecessors fetch status */ - predecessorsStatus: LoadingState; + predecessorsStatus: LoadingStatusEntry; /** * Successors fetch status */ - successorsStatus: LoadingState; + successorsStatus: LoadingStatusEntry; } export enum LoadingStatus { @@ -48,19 +48,24 @@ export enum FailureReason { INVALID_TIEBREAKER = 'invalid_tiebreaker', } -export type LoadingStatusEntry = Partial<{ - status: LoadingStatus; - reason: FailureReason; - error: Error; -}>; +export interface LoadingStatusEntry { + value: LoadingStatus; + error?: Error; + reason?: FailureReason; +} -export type LoadingState = LoadingStatus | LoadingStatusEntry; +export enum FetchType { + ALL, + CONTEXT, + PREDECESSORS, + SUCCESSORS, +} export const getInitialContextQueryState = (): ContextFetchState => ({ anchor: {} as EsHitRecord, predecessors: [], successors: [], - anchorStatus: LoadingStatus.UNINITIALIZED, - predecessorsStatus: LoadingStatus.UNINITIALIZED, - successorsStatus: LoadingStatus.UNINITIALIZED, + anchorStatus: { value: LoadingStatus.UNINITIALIZED }, + predecessorsStatus: { value: LoadingStatus.UNINITIALIZED }, + successorsStatus: { value: LoadingStatus.UNINITIALIZED }, }); diff --git a/src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx b/src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx index 944444d6ba054..6766ad03aea35 100644 --- a/src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx +++ b/src/plugins/discover/public/application/components/context_app/__mocks__/top_nav_menu.tsx @@ -8,4 +8,4 @@ import React from 'react'; -export const TopNavMenuMock = () =>
Hello World
; +export const mockTopNavMenu = () =>
Hello World
; diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx new file mode 100644 index 0000000000000..0d8b98afc5472 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx @@ -0,0 +1,86 @@ +/* + * 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 from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; +import { mockTopNavMenu } from './__mocks__/top_nav_menu'; +import { ContextAppContent } from './context_app_content'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { ContextApp } from './context_app'; + +jest.mock('../../../kibana_services', () => { + return { + getServices: () => ({ + data: { search: { searchSource: { createEmpty: () => {} } } }, + indexPatterns: {}, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + navigation: { ui: { TopNavMenu: mockTopNavMenu } }, + }), + }; +}); + +jest.mock('./use_context_app_state', () => { + return { + useContextAppState: () => ({ + setAppState: () => {}, + stateContainer: {}, + appState: { sort: [[1603114502000, 2092]] }, + }), + }; +}); + +jest.mock('./use_context_app_fetch', () => { + return { + useContextAppFetch: () => ({ + fetchedState: { + anchor: { _id: 'mock_id', fields: [] }, + predecessors: [], + successors: [], + anchorStatus: { value: 'loaded' }, + predecessorsStatus: { value: 'loaded' }, + successorsStatus: { value: 'loaded' }, + }, + fetchAllRows: () => {}, + fetchContextRows: () => {}, + fetchSurroundingRows: () => {}, + }), + }; +}); + +describe('ContextApp test', () => { + const defaultProps = { + indexPattern: indexPatternMock, + indexPatternId: 'mocked_index_pattern', + anchorId: 'mocked_anchor_id', + }; + + const topNavProps = { + appName: 'context', + showSearchBar: true, + showQueryBar: false, + showFilterBar: true, + showSaveQuery: false, + showDatePicker: false, + indexPatterns: [indexPatternMock], + useDefaultBehaviors: true, + }; + + it('renders correctly', () => { + const component = mountWithIntl(); + expect(component.find(ContextAppContent).length).toBe(1); + const topNavMenu = component.find(mockTopNavMenu); + expect(topNavMenu.length).toBe(1); + expect(topNavMenu.props()).toStrictEqual(topNavProps); + }); +}); diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 591a6bdf3c239..570fcf036d1d2 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -8,7 +8,6 @@ import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react'; import classNames from 'classnames'; -import './context_app.scss'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; @@ -20,7 +19,7 @@ import { } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; -import { LoadingStatus, LoadingStatusEntry } from '../../angular/context_query_state'; +import { LoadingStatus } from '../../angular/context_query_state'; import { getServices } from '../../../kibana_services'; import { AppState, isEqualFilters } from '../../angular/context_state'; import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; @@ -60,14 +59,16 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp /** * Context fetched state */ - const { fetchedState, fetchMoreRows, fetchContextRows, fetchAllRows } = useContextAppFetch({ - anchorId, - indexPatternId, - indexPattern, - appState, - useNewFieldsApi, - services, - }); + const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows } = useContextAppFetch( + { + anchorId, + indexPatternId, + indexPattern, + appState, + useNewFieldsApi, + services, + } + ); /** * Fetch docs on ui changes @@ -76,15 +77,15 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp if (!prevState.current) { fetchAllRows(); } else if (prevState.current.predecessorCount !== appState.predecessorCount) { - fetchMoreRows('predecessors'); + fetchSurroundingRows('predecessors'); } else if (prevState.current.successorCount !== appState.successorCount) { - fetchMoreRows('successors'); + fetchSurroundingRows('successors'); } else if (!isEqualFilters(prevState.current.filters, appState.filters)) { fetchContextRows(); } prevState.current = cloneDeep(appState); - }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchMoreRows]); + }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, @@ -135,11 +136,8 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp return ( - {fetchedState.anchorStatus === LoadingStatus.FAILED ? ( - + {fetchedState.anchorStatus.value === LoadingStatus.FAILED ? ( + ) : ( @@ -169,9 +167,9 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp sort={appState.sort} predecessors={fetchedState.predecessors} successors={fetchedState.successors} - anchorStatus={fetchedState.anchorStatus} - predecessorsStatus={fetchedState.predecessorsStatus} - successorsStatus={fetchedState.successorsStatus} + anchorStatus={fetchedState.anchorStatus.value} + predecessorsStatus={fetchedState.predecessorsStatus.value} + successorsStatus={fetchedState.successorsStatus.value} defaultStepSize={defaultStepSize} useNewFieldsApi={useNewFieldsApi} isLegacy={isLegacy} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx index 54c9d43167768..0536ab7e6a025 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.test.tsx @@ -9,33 +9,27 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; -import { IndexPattern } from '../../../../../data/common/index_patterns'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; -import { ContextErrorMessage } from '../context_error_message'; -import { TopNavMenuMock } from './__mocks__/top_nav_menu'; import { AppState, GetStateReturn } from '../../angular/context_state'; import { SortDirection } from 'src/plugins/data/common'; import { EsHitRecordList } from '../../angular/context/api/context'; +import { ContextAppContent, ContextAppContentProps } from './context_app_content'; +import { getServices } from '../../../kibana_services'; +import { LoadingStatus } from '../../angular/context_query_state'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { DiscoverGrid } from '../discover_grid/discover_grid'; jest.mock('../../../kibana_services', () => { return { getServices: () => ({ - metadata: { - branch: 'test', - }, - capabilities: { - discover: { - save: true, - }, - }, uiSettings: mockUiSettings, }), }; }); -describe('ContextAppLegacy test', () => { +describe('ContextAppContent test', () => { const hit = { _id: '123', _index: 'test_index', @@ -52,74 +46,59 @@ describe('ContextAppLegacy test', () => { fields: [{ order_date: ['2020-10-19T13:35:02.000Z'] }], sort: [1603114502000, 2092], }; - const indexPattern = { - id: 'test_index_pattern', - } as IndexPattern; - const defaultProps = { - columns: ['_source'], - filter: () => {}, - hits: ([hit] as unknown) as EsHitRecordList, - sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>, - minimumVisibleRows: 5, - indexPattern, + const defaultProps = ({ + columns: ['Time (@timestamp)', '_source'], + indexPattern: indexPatternMock, appState: ({} as unknown) as AppState, stateContainer: ({} as unknown) as GetStateReturn, - anchorStatus: 'loaded', - anchorReason: 'no reason', + anchorStatus: LoadingStatus.LOADED, + predecessorsStatus: LoadingStatus.LOADED, + successorsStatus: LoadingStatus.LOADED, + rows: ([hit] as unknown) as EsHitRecordList, + predecessors: [], + successors: [], defaultStepSize: 5, predecessorCount: 10, successorCount: 10, - predecessorAvailable: 10, - successorAvailable: 10, - onChangePredecessorCount: jest.fn(), - onChangeSuccessorCount: jest.fn(), - predecessorStatus: 'loaded', - successorStatus: 'loaded', - topNavMenu: TopNavMenuMock, useNewFieldsApi: false, isPaginationEnabled: false, - }; - const topNavProps = { - appName: 'context', - showSearchBar: true, - showQueryBar: false, - showFilterBar: true, - showSaveQuery: false, - showDatePicker: false, - indexPatterns: [indexPattern], - useDefaultBehaviors: true, - }; + onAddColumn: () => {}, + onRemoveColumn: () => {}, + onSetColumns: () => {}, + services: getServices(), + sort: [['order_date', 'desc']] as Array<[string, SortDirection]>, + isLegacy: true, + setAppState: () => {}, + addFilter: () => {}, + } as unknown) as ContextAppContentProps; - it('renders correctly', () => { - const component = mountWithIntl(); + it('should render legacy table correctly', () => { + const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(1); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(0); expect(component.find(ActionBar).length).toBe(2); - const topNavMenu = component.find(TopNavMenuMock); - expect(topNavMenu.length).toBe(1); - expect(topNavMenu.props()).toStrictEqual(topNavProps); }); it('renders loading indicator', () => { const props = { ...defaultProps }; - props.anchorStatus = 'loading'; - const component = mountWithIntl(); + props.anchorStatus = LoadingStatus.LOADING; + const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); expect(loadingIndicator.length).toBe(1); - expect(component.find(ActionBar).length).toBe(2); - expect(component.find(TopNavMenuMock).length).toBe(1); }); it('renders error message', () => { const props = { ...defaultProps }; - props.anchorStatus = 'failed'; - props.anchorReason = 'something went wrong'; - const component = mountWithIntl(); + props.anchorStatus = LoadingStatus.FAILED; + const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); - expect(component.find(TopNavMenuMock).length).toBe(0); - const errorMessage = component.find(ContextErrorMessage); - expect(errorMessage.length).toBe(1); + }); + + it('should render discover grid correctly', () => { + const props = { ...defaultProps, isLegacy: false }; + const component = mountWithIntl(); + expect(component.find(DiscoverGrid).length).toBe(1); }); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 446fdd4a3ffeb..45724c4f9793e 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, Fragment } from 'react'; +import React, { useState, Fragment, useMemo, useCallback } from 'react'; import './context_app_content.scss'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; @@ -16,8 +16,8 @@ import { DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; -import { LoadingState, LoadingStatus } from '../../angular/context_query_state'; -import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; +import { LoadingStatus } from '../../angular/context_query_state'; +import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; @@ -38,9 +38,9 @@ export interface ContextAppContentProps { sort: SortPairArr[]; predecessors: EsHitRecordList; successors: EsHitRecordList; - anchorStatus: LoadingState; - predecessorsStatus: LoadingState; - successorsStatus: LoadingState; + anchorStatus: LoadingStatus; + predecessorsStatus: LoadingStatus; + successorsStatus: LoadingStatus; useNewFieldsApi: boolean; defaultStepSize: number; isLegacy: boolean; @@ -53,12 +53,6 @@ export interface ContextAppContentProps { } const DataGridMemoized = React.memo(DiscoverGrid); -const PREDECESSOR_TYPE = 'predecessors'; -const SUCCESSOR_TYPE = 'successors'; - -function isLoading(status: LoadingState) { - return status !== LoadingStatus.LOADED && status !== LoadingStatus.FAILED; -} export function ContextAppContent({ columns, @@ -83,26 +77,20 @@ export function ContextAppContent({ addFilter, }: ContextAppContentProps) { const { uiSettings: config } = services; + const [expandedDoc, setExpandedDoc] = useState(undefined); const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const isAnchorLoading = anchorStatus === LoadingStatus.LOADING; + const arePredecessorsLoading = + predecessorsStatus === LoadingStatus.LOADING || + predecessorsStatus === LoadingStatus.UNINITIALIZED; + const areSuccessorsLoading = + successorsStatus === LoadingStatus.LOADING || successorsStatus === LoadingStatus.UNINITIALIZED; - const actionBarProps = (type: string) => { - const isPredecessorType = type === PREDECESSOR_TYPE; - return { - defaultStepSize, - docCount: isPredecessorType ? predecessorCount : successorCount || defaultStepSize, - docCountAvailable: isPredecessorType ? predecessors.length : successors.length, - onChangeCount: (count) => { - const countKey = type === PREDECESSOR_TYPE ? 'predecessorCount' : 'successorCount'; - setAppState({ [countKey]: count }); - }, - isLoading: isPredecessorType - ? isLoading(predecessorsStatus || LoadingStatus.UNINITIALIZED) - : isLoading(successorsStatus || LoadingStatus.UNINITIALIZED), - type, - isDisabled: !isAnchorLoaded, - } as ActionBarProps; - }; + const showTimeCol = useMemo( + () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + [config, indexPattern] + ); const docTableProps = () => { return { @@ -111,11 +99,11 @@ export function ContextAppContent({ rows: rows as ElasticSearchHit[], indexPattern, expandedDoc, - isLoading: isLoading(anchorStatus || LoadingStatus.UNINITIALIZED), + isLoading: isAnchorLoading, sampleSize: 0, sort, isSortEnabled: false, - showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + showTimeCol, services, useNewFieldsApi, isPaginationEnabled: false, @@ -144,7 +132,10 @@ export function ContextAppContent({ }; const loadingFeedback = () => { - if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { + if ( + isLegacy && + (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) + ) { return ( @@ -154,24 +145,47 @@ export function ContextAppContent({ return null; }; + const onChangeCount = useCallback( + (type, count) => { + const countKey = type === 'successors' ? 'successorCount' : 'predecessorCount'; + setAppState({ [countKey]: count }); + }, + [setAppState] + ); + return ( - - {isLegacy && loadingFeedback()} + + {loadingFeedback()} - {isLegacy ? ( - isAnchorLoaded && ( -
- -
- ) - ) : ( + {isLegacy && isAnchorLoaded && ( +
+ +
+ )} + {!isLegacy && (
)} - +
); } diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 3307199e27353..ba3a56c75b488 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -24,8 +24,8 @@ import { import { AppState } from '../../angular/context_state'; import { getFirstSortableField } from '../../angular/context/api/utils/sorting'; -const createUnknownError = (statusKey: string, error: Error) => ({ - [statusKey]: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, +const createError = (statusKey: string, reason: FailureReason, error?: Error) => ({ + [statusKey]: { value: LoadingStatus.FAILED, error, reason }, }); export function useContextAppFetch({ @@ -68,21 +68,38 @@ export function useContextAppFetch({ setFetchedState((prevState) => ({ ...prevState, ...values })); }, []); + const sendInvalidTieBreakerFeedback = useCallback(() => { + const message = i18n.translate('discover.context.invalidTieBreakerFiledSetting', { + defaultMessage: 'Invalid tie breaker field setting', + }); + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load documents', + }), + text: toMountPoint({message}), + }); + }, [toastNotifications]); + const fetchAnchorRow = useCallback(async () => { const { sort } = appState; const [[, sortDir]] = sort; + if (!tieBreakerField) { + setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER)); + sendInvalidTieBreakerFeedback(); + return; + } + try { + setState({ anchorStatus: { value: LoadingStatus.LOADING } }); const anchor = await fetchAnchor(indexPatternId, anchorId, [ fromPairs(sort), { [tieBreakerField]: sortDir }, ]); - setState({ anchor, anchorStatus: LoadingStatus.LOADED }); + setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } }); return anchor; } catch (error) { - setState({ - anchorStatus: { status: LoadingStatus.FAILED, reason: FailureReason.UNKNOWN, error }, - }); + setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', @@ -92,11 +109,12 @@ export function useContextAppFetch({ } }, [ appState, + tieBreakerField, + setState, + sendInvalidTieBreakerFeedback, fetchAnchor, indexPatternId, anchorId, - tieBreakerField, - setState, toastNotifications, ]); @@ -108,9 +126,17 @@ export function useContextAppFetch({ const count = type === 'predecessors' ? appState.predecessorCount : appState.successorCount; const anchor = fetchedAnchor || fetchedState.anchor; + const statusKey = `${type}Status`; + + if (!tieBreakerField) { + setState(createError(statusKey, FailureReason.INVALID_TIEBREAKER)); + sendInvalidTieBreakerFeedback(); + return; + } try { - return await fetchSurroundingDocs( + setState({ [statusKey]: { value: LoadingStatus.LOADING } }); + const rows = await fetchSurroundingDocs( type, indexPatternId, anchor as EsHitRecord, @@ -120,7 +146,9 @@ export function useContextAppFetch({ count, filters ); + setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } }); } catch (error) { + setState(createError(statusKey, FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', @@ -133,42 +161,22 @@ export function useContextAppFetch({ filterManager, appState, fetchedState.anchor, + tieBreakerField, + setState, + sendInvalidTieBreakerFeedback, fetchSurroundingDocs, indexPatternId, - tieBreakerField, toastNotifications, ] ); const fetchContextRows = useCallback( - async (anchor?: EsHitRecord) => { - setState({ - predecessorsStatus: LoadingStatus.LOADING, - successorsStatus: LoadingStatus.LOADING, - }); - - const [predecessors, successors] = await Promise.allSettled([ + (anchor?: EsHitRecord) => + Promise.allSettled([ fetchSurroundingRows('predecessors', anchor), fetchSurroundingRows('successors', anchor), - ]); - - const predecessorsStatus = - predecessors.status === 'fulfilled' - ? LoadingStatus.LOADED - : createUnknownError('predecessorsStatus', predecessors.reason); - const successorsStatus = - successors.status === 'fulfilled' - ? LoadingStatus.LOADED - : createUnknownError('successorsStatus', successors.reason); - - setState({ - predecessorsStatus, - successorsStatus, - successors: successors.status === 'fulfilled' ? successors.value : [], - predecessors: predecessors.status === 'fulfilled' ? predecessors.value : [], - }); - }, - [fetchSurroundingRows, setState] + ]), + [fetchSurroundingRows] ); const fetchAllRows = useCallback( @@ -176,25 +184,10 @@ export function useContextAppFetch({ [fetchAnchorRow, fetchContextRows] ); - const fetchMoreRows = useCallback( - async (type) => { - const statusKey = `${type}Status`; - setState({ [statusKey]: LoadingStatus.LOADING }); - - try { - const rows = await fetchSurroundingRows(type); - setState({ [type]: rows, [statusKey]: LoadingStatus.LOADED }); - } catch (error) { - setState(createUnknownError(statusKey, error)); - } - }, - [fetchSurroundingRows, setState] - ); - return { fetchedState, - fetchMoreRows, - fetchContextRows, fetchAllRows, + fetchContextRows, + fetchSurroundingRows, }; } 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 0a75ae34d958f..7ac1ef60ed3c2 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,21 +9,21 @@ import React from 'react'; import { EuiCallOut, EuiText } from '@elastic/eui'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; +import { + FailureReason, + LoadingStatus, + LoadingStatusEntry, +} from '../../angular/context_query_state'; export interface ContextErrorMessageProps { /** * the status of the loading action */ - status: string; - /** - * the reason of the error - */ - reason?: string; + status: LoadingStatusEntry; } -export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps) { - if (status !== LoadingStatus.FAILED) { +export function ContextErrorMessage({ status }: ContextErrorMessageProps) { + if (status.value !== LoadingStatus.FAILED) { return null; } return ( @@ -40,7 +40,7 @@ export function ContextErrorMessage({ status, reason }: ContextErrorMessageProps data-test-subj="contextErrorMessageTitle" > - {reason === FailureReason.UNKNOWN && ( + {status.reason === FailureReason.UNKNOWN && ( Date: Tue, 8 Jun 2021 00:27:08 +0300 Subject: [PATCH 23/36] [Discover] add tests for fetching methods, remove redundant files --- .../angular/context/query/actions.tsx | 210 ---------------- .../angular/context/query/index.ts | 10 - .../angular/context/query/state.ts | 17 -- .../application/angular/context_state.test.ts | 2 +- .../application/angular/context_state.ts | 2 +- .../__mocks__/use_context_app_fetch.tsx | 64 +++++ .../components/context_app/context_app.tsx | 4 +- .../context_app/context_app_content.tsx | 7 +- .../context_app/use_context_app_fetch.test.ts | 224 ++++++++++++++++++ .../context_app/use_context_app_fetch.tsx | 23 +- .../context_error_message.test.tsx | 10 +- 11 files changed, 314 insertions(+), 259 deletions(-) delete mode 100644 src/plugins/discover/public/application/angular/context/query/actions.tsx delete mode 100644 src/plugins/discover/public/application/angular/context/query/index.ts delete mode 100644 src/plugins/discover/public/application/angular/context/query/state.ts create mode 100644 src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx create mode 100644 src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts diff --git a/src/plugins/discover/public/application/angular/context/query/actions.tsx b/src/plugins/discover/public/application/angular/context/query/actions.tsx deleted file mode 100644 index fb530874d1e3b..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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 from 'react'; -import { fromPairs } from 'lodash'; -import { i18n } from '@kbn/i18n'; - -import { getServices } from '../../../../kibana_services'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; -import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; -import { fetchAnchorProvider } from '../api/anchor'; -import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; -import { getQueryParameterActions } from '../query_parameters'; -import { - ContextQueryState, - FailureReason, - LoadingStatus, - LoadingStatusEntry, - LoadingStatusState, - QueryParameters, -} from '../../context_query_state'; - -interface DiscoverPromise extends PromiseConstructor { - try: (fn: () => Promise) => Promise; -} - -export function QueryActionsProvider(Promise: DiscoverPromise) { - const { filterManager, indexPatterns, data, uiSettings } = getServices(); - const useNewFieldsApi = !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); - const fetchAnchor = fetchAnchorProvider( - indexPatterns, - data.search.searchSource.createEmpty(), - useNewFieldsApi - ); - const { fetchSurroundingDocs } = fetchContextProvider(indexPatterns, useNewFieldsApi); - const { setPredecessorCount, setQueryParameters, setSuccessorCount } = getQueryParameterActions( - filterManager, - indexPatterns - ); - - const setFailedStatus = (state: ContextQueryState) => ( - subject: keyof LoadingStatusState, - details: LoadingStatusEntry = {} - ) => - (state.loadingStatus[subject] = { - status: LoadingStatus.FAILED, - reason: FailureReason.UNKNOWN, - ...details, - }); - - const setLoadedStatus = (state: ContextQueryState) => (subject: keyof LoadingStatusState) => - (state.loadingStatus[subject] = { - status: LoadingStatus.LOADED, - }); - - const setLoadingStatus = (state: ContextQueryState) => (subject: keyof LoadingStatusState) => - (state.loadingStatus[subject] = { - status: LoadingStatus.LOADING, - }); - - const fetchAnchorRow = (state: ContextQueryState) => () => { - const { - queryParameters: { indexPatternId, anchorId, sort, tieBreakerField }, - } = state; - - if (!tieBreakerField) { - return Promise.reject( - setFailedStatus(state)('anchor', { - reason: FailureReason.INVALID_TIEBREAKER, - }) - ); - } - - setLoadingStatus(state)('anchor'); - const [[, sortDir]] = sort; - - return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) - ).then( - (anchorDocument: EsHitRecord) => { - setLoadedStatus(state)('anchor'); - state.rows.anchor = anchorDocument; - return anchorDocument; - }, - (error: Error) => { - setFailedStatus(state)('anchor', { error }); - getServices().toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { - defaultMessage: 'Unable to load the anchor document', - }), - text: toMountPoint({error.message}), - }); - throw error; - } - ); - }; - - const fetchSurroundingRows = (type: SurrDocType, state: ContextQueryState) => { - const { - queryParameters: { indexPatternId, sort, tieBreakerField }, - rows: { anchor }, - } = state; - const filters = getServices().filterManager.getFilters(); - - const count = - type === 'successors' - ? state.queryParameters.successorCount - : state.queryParameters.predecessorCount; - - if (!tieBreakerField) { - return Promise.reject( - setFailedStatus(state)(type, { - reason: FailureReason.INVALID_TIEBREAKER, - }) - ); - } - - setLoadingStatus(state)(type); - const [[sortField, sortDir]] = sort; - - return Promise.try(() => - fetchSurroundingDocs( - type, - indexPatternId, - anchor, - sortField, - tieBreakerField, - sortDir, - count, - filters - ) - ).then( - (documents: EsHitRecordList) => { - setLoadedStatus(state)(type); - state.rows[type] = documents; - return documents; - }, - (error: Error) => { - setFailedStatus(state)(type, { error }); - getServices().toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), - text: toMountPoint({error.message}), - }); - throw error; - } - ); - }; - - const fetchContextRows = (state: ContextQueryState) => () => - Promise.all([ - fetchSurroundingRows('predecessors', state), - fetchSurroundingRows('successors', state), - ]); - - const fetchAllRows = (state: ContextQueryState) => () => - Promise.try(fetchAnchorRow(state)).then(fetchContextRows(state)); - - const fetchContextRowsWithNewQueryParameters = (state: ContextQueryState) => ( - queryParameters: QueryParameters - ) => { - setQueryParameters(state)(queryParameters); - return fetchContextRows(state)(); - }; - - const fetchAllRowsWithNewQueryParameters = (state: ContextQueryState) => ( - queryParameters: QueryParameters - ) => { - setQueryParameters(state)(queryParameters); - return fetchAllRows(state)(); - }; - - const fetchGivenPredecessorRows = (state: ContextQueryState) => (count: number) => { - setPredecessorCount(state)(count); - return fetchSurroundingRows('predecessors', state); - }; - - const fetchGivenSuccessorRows = (state: ContextQueryState) => (count: number) => { - setSuccessorCount(state)(count); - return fetchSurroundingRows('successors', state); - }; - - const setAllRows = (state: ContextQueryState) => ( - predecessorRows: EsHitRecordList, - anchorRow: EsHitRecord, - successorRows: EsHitRecordList - ) => - (state.rows.all = [ - ...(predecessorRows || []), - ...(anchorRow ? [anchorRow] : []), - ...(successorRows || []), - ]); - - return { - fetchAllRows, - fetchAllRowsWithNewQueryParameters, - fetchAnchorRow, - fetchContextRows, - fetchContextRowsWithNewQueryParameters, - fetchGivenPredecessorRows, - fetchGivenSuccessorRows, - setAllRows, - }; -} diff --git a/src/plugins/discover/public/application/angular/context/query/index.ts b/src/plugins/discover/public/application/angular/context/query/index.ts deleted file mode 100644 index 70e3dc1600472..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * 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. - */ - -export { QueryActionsProvider } from './actions'; -export { createInitialLoadingStatusState } from './state'; diff --git a/src/plugins/discover/public/application/angular/context/query/state.ts b/src/plugins/discover/public/application/angular/context/query/state.ts deleted file mode 100644 index 43f9e2c66d67c..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query/state.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { LoadingStatus, LoadingStatusState } from '../../context_query_state'; - -export function createInitialLoadingStatusState(): LoadingStatusState { - return { - anchor: LoadingStatus.UNINITIALIZED, - predecessors: LoadingStatus.UNINITIALIZED, - successors: LoadingStatus.UNINITIALIZED, - }; -} diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index e9294567032c4..a22ce479c6d78 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -23,7 +23,7 @@ describe('Test Discover Context State', () => { history = createBrowserHistory(); history.push('/'); state = getState({ - defaultStepSize: '4', + defaultStepSize: 4, timeFieldName: 'time', history, uiSettings: { diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 7c2e0f3a13399..26e2f6e4a1b43 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -35,7 +35,7 @@ export interface AppState { /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ - sort: [[string, SortDirection]]; + sort: string[][]; /** * Number of records to be fetched after the anchor records (older records) */ diff --git a/src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx new file mode 100644 index 0000000000000..1499067d2332e --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/__mocks__/use_context_app_fetch.tsx @@ -0,0 +1,64 @@ +/* + * 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. + */ + +export const mockAnchorHit = { + _id: '123', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T18:52:17.000Z'] }, + sort: [1623091937000, 2092], + isAnchor: true, + _version: 1, +}; + +export const mockPredecessorHits = [ + { + _id: '1', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T19:14:29.000Z'] }, + sort: ['2021-06-07T19:14:29.000Z', 2092], + _version: 1, + }, + { + _id: '2', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T19:14:12.000Z'] }, + sort: ['2021-06-07T19:14:12.000Z', 2431], + _version: 1, + }, + { + _id: '3', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T19:10:22.000Z'] }, + sort: ['2021-06-07T19:10:22.000Z', 2435], + _version: 1, + }, +]; + +export const mockSuccessorHits = [ + { + _id: '11', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T18:49:39.000Z'] }, + sort: ['2021-06-07T18:49:39.000Z', 2382], + _version: 1, + }, + { + _id: '22', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T18:48:28.000Z'] }, + sort: ['2021-06-07T18:48:28.000Z', 2631], + _version: 1, + }, + { + _id: '33', + _index: 'the-index-pattern-id', + fields: { order_date: ['2021-06-07T18:47:16.000Z'] }, + sort: ['2021-06-07T18:47:16.000Z', 2437], + _version: 1, + }, +]; diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 570fcf036d1d2..68805aec65530 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -11,7 +11,7 @@ import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { esFilters } from '../../../../../data/public'; +import { esFilters, SortDirection } from '../../../../../data/public'; import { CONTEXT_DEFAULT_SIZE_SETTING, DOC_TABLE_LEGACY, @@ -164,7 +164,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} rows={rows} - sort={appState.sort} + sort={appState.sort as [[string, SortDirection]]} predecessors={fetchedState.predecessors} successors={fetchedState.successors} anchorStatus={fetchedState.anchorStatus.value} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 45724c4f9793e..c15f24a58f4f0 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -11,11 +11,12 @@ import './context_app_content.scss'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; +import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { SortDirection } from '../../../../../data/public'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; import { LoadingStatus } from '../../angular/context_query_state'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; @@ -35,7 +36,7 @@ export interface ContextAppContentProps { predecessorCount: number; successorCount: number; rows: EsHitRecordList; - sort: SortPairArr[]; + sort: [[string, SortDirection]]; predecessors: EsHitRecordList; successors: EsHitRecordList; anchorStatus: LoadingStatus; @@ -101,7 +102,7 @@ export function ContextAppContent({ expandedDoc, isLoading: isAnchorLoading, sampleSize: 0, - sort, + sort: sort as [[string, SortDirection]], isSortEnabled: false, showTimeCol, services, diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts new file mode 100644 index 0000000000000..2c519e7e41b0c --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts @@ -0,0 +1,224 @@ +/* + * 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 { act, renderHook } from '@testing-library/react-hooks'; +import { setServices, getServices } from '../../../kibana_services'; +import { SortDirection } from '../../../../../data/public'; +import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; +import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; +import { DiscoverServices } from '../../../build_services'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { FailureReason, LoadingStatus } from '../../angular/context_query_state'; +import { ContextAppFetchProps, useContextAppFetch } from './use_context_app_fetch'; +import { + mockAnchorHit, + mockPredecessorHits, + mockSuccessorHits, +} from './__mocks__/use_context_app_fetch'; + +const mockFilterManager = createFilterManagerMock(); + +jest.mock('../../angular/context/api/context', () => ({ + fetchContextProvider: () => ({ + fetchSurroundingDocs: (type: string, indexPatternId: string) => { + if (!indexPatternId) { + throw new Error(); + } + return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; + }, + }), +})); + +jest.mock('../../angular/context/api/anchor', () => ({ + fetchAnchorProvider: () => (indexPatternId: string) => { + if (!indexPatternId) { + throw new Error(); + } + return mockAnchorHit; + }, +})); + +const initDefaults = (tieBreakerFields: string[], indexPatternId = 'the-index-pattern-id') => { + const dangerNotification = jest.fn(); + + setServices(({ + data: { + search: { + searchSource: { + createEmpty: jest.fn(), + }, + }, + }, + indexPatterns: indexPatternsMock, + toastNotifications: { addDanger: dangerNotification }, + core: { notifications: { toasts: [] } }, + history: () => {}, + filterManager: mockFilterManager, + uiSettings: { + get: (key: string) => { + if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) { + return tieBreakerFields; + } + }, + }, + } as unknown) as DiscoverServices); + + return { + dangerNotification, + props: ({ + anchorId: 'mock_anchor_id', + indexPatternId, + indexPattern: indexPatternMock, + appState: { + sort: [['order_date', SortDirection.desc]], + predecessorCount: 2, + successorCount: 2, + }, + useNewFieldsApi: false, + services: getServices(), + } as unknown) as ContextAppFetchProps, + }; +}; + +describe('test useContextAppFetch', () => { + it('should fetch all correctly', async () => { + const { props } = initDefaults(['_doc']); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchAllRows(); + }); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.anchor).toEqual({ ...mockAnchorHit, isAnchor: true }); + expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits); + expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); + }); + + it('should set anchorStatus to failed when tieBreakingField array is empty', async () => { + const { props, dangerNotification } = initDefaults([]); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchAllRows(); + }); + + expect(dangerNotification.mock.calls.length).toBe(1); + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.FAILED); + expect(result.current.fetchedState.anchorStatus.reason).toBe(FailureReason.INVALID_TIEBREAKER); + expect(result.current.fetchedState.anchor).toEqual({}); + expect(result.current.fetchedState.predecessors).toEqual([]); + expect(result.current.fetchedState.successors).toEqual([]); + }); + + it('should set anchorStatus to failed when invalid indexPatternId provided', async () => { + const { props, dangerNotification } = initDefaults(['_doc'], ''); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchAllRows(); + }); + + expect(dangerNotification.mock.calls.length).toBe(1); + expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.FAILED); + expect(result.current.fetchedState.anchorStatus.reason).toBe(FailureReason.UNKNOWN); + expect(result.current.fetchedState.anchor).toEqual({}); + expect(result.current.fetchedState.predecessors).toEqual([]); + expect(result.current.fetchedState.successors).toEqual([]); + }); + + it('should fetch context rows correctly', async () => { + const { props } = initDefaults(['_doc']); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchContextRows(mockAnchorHit); + }); + + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.LOADED); + expect(result.current.fetchedState.predecessors).toEqual(mockPredecessorHits); + expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); + }); + + it('should set context rows statuses to failed when tieBreakingField array is empty', async () => { + const { props, dangerNotification } = initDefaults([]); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchContextRows(mockAnchorHit); + }); + + expect(dangerNotification.mock.calls.length).toBe(2); // for successors and predecessors + expect(result.current.fetchedState.predecessorsStatus).toEqual({ + value: LoadingStatus.FAILED, + reason: FailureReason.INVALID_TIEBREAKER, + }); + expect(result.current.fetchedState.successorsStatus).toEqual({ + value: LoadingStatus.FAILED, + reason: FailureReason.INVALID_TIEBREAKER, + }); + expect(result.current.fetchedState.predecessors).toEqual([]); + expect(result.current.fetchedState.successors).toEqual([]); + }); + + it('should set context rows statuses to failed when invalid indexPatternId provided', async () => { + const { props, dangerNotification } = initDefaults(['_doc'], ''); + + const { result } = renderHook(() => { + return useContextAppFetch(props); + }); + + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); + + await act(async () => { + await result.current.fetchContextRows(mockAnchorHit); + }); + + expect(dangerNotification.mock.calls.length).toBe(2); // for successors and predecessors + expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.FAILED); + expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.FAILED); + expect(result.current.fetchedState.successorsStatus.reason).toBe(FailureReason.UNKNOWN); + expect(result.current.fetchedState.successorsStatus.reason).toBe(FailureReason.UNKNOWN); + expect(result.current.fetchedState.predecessors).toEqual([]); + expect(result.current.fetchedState.successors).toEqual([]); + }); +}); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index ba3a56c75b488..762aad9b5d691 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -7,7 +7,6 @@ */ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; - import { fromPairs } from 'lodash'; import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../common'; import { DiscoverServices } from '../../../build_services'; @@ -28,6 +27,15 @@ const createError = (statusKey: string, reason: FailureReason, error?: Error) => [statusKey]: { value: LoadingStatus.FAILED, error, reason }, }); +export interface ContextAppFetchProps { + anchorId: string; + indexPatternId: string; + indexPattern: IndexPattern; + appState: AppState; + useNewFieldsApi: boolean; + services: DiscoverServices; +} + export function useContextAppFetch({ anchorId, indexPatternId, @@ -35,14 +43,7 @@ export function useContextAppFetch({ appState, useNewFieldsApi, services, -}: { - anchorId: string; - indexPatternId: string; - indexPattern: IndexPattern; - appState: AppState; - useNewFieldsApi: boolean; - services: DiscoverServices; -}) { +}: ContextAppFetchProps) { const { uiSettings: config, data, indexPatterns, toastNotifications, filterManager } = services; const searchSource = useMemo(() => { @@ -99,13 +100,13 @@ export function useContextAppFetch({ setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } }); return anchor; } catch (error) { - setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', }), text: toMountPoint({error.message}), }); + setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); } }, [ appState, @@ -148,13 +149,13 @@ export function useContextAppFetch({ ); setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } }); } catch (error) { - setState(createError(statusKey, FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ title: i18n.translate('discover.context.unableToLoadDocumentDescription', { defaultMessage: 'Unable to load documents', }), text: toMountPoint({error.message}), }); + setState(createError(statusKey, FailureReason.UNKNOWN, error)); } }, [ 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 91d77c76d828c..7f7d92be03103 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 @@ -17,24 +17,26 @@ describe('loading spinner', function () { let component: ReactWrapper; it('ContextErrorMessage does not render on loading', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); }); it('ContextErrorMessage does not render on success loading', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(0); }); it('ContextErrorMessage renders just the title if the reason is not specifically handled', () => { - component = mountWithIntl(); + component = mountWithIntl(); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); expect(findTestSubject(component, 'contextErrorMessageBody').text()).toBe(''); }); it('ContextErrorMessage renders the reason for unknown errors', () => { component = mountWithIntl( - + ); expect(findTestSubject(component, 'contextErrorMessageTitle').length).toBe(1); expect(findTestSubject(component, 'contextErrorMessageBody').length).toBe(1); From 358201ef0e2133b96973467dcdc4d4dc3b624174 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 10:45:56 +0300 Subject: [PATCH 24/36] [Discover] remove redundant angular utils, add filter test --- .../public/__mocks__/index_patterns.ts | 3 +- .../components/action_bar/action_bar.test.tsx | 11 +- .../components/action_bar/action_bar.tsx | 5 +- .../context/query_parameters/actions.test.ts | 160 ------------------ .../context/query_parameters/actions.ts | 82 --------- .../angular/context/query_parameters/state.ts | 26 --- .../angular/context_query_state.ts | 7 - .../context_app/context_app.test.tsx | 125 ++++++++------ .../context_app/context_app_content.tsx | 4 +- .../context_app/use_context_app_fetch.tsx | 4 +- .../context_app/utils/clamp.ts} | 8 +- .../context_app/utils}/constants.ts | 3 - 12 files changed, 97 insertions(+), 341 deletions(-) delete mode 100644 src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts delete mode 100644 src/plugins/discover/public/application/angular/context/query_parameters/actions.ts delete mode 100644 src/plugins/discover/public/application/angular/context/query_parameters/state.ts rename src/plugins/discover/public/application/{angular/context/query_parameters/index.ts => components/context_app/utils/clamp.ts} (63%) rename src/plugins/discover/public/application/{angular/context/query_parameters => components/context_app/utils}/constants.ts (76%) diff --git a/src/plugins/discover/public/__mocks__/index_patterns.ts b/src/plugins/discover/public/__mocks__/index_patterns.ts index b8faab99de811..3425ea42f4e62 100644 --- a/src/plugins/discover/public/__mocks__/index_patterns.ts +++ b/src/plugins/discover/public/__mocks__/index_patterns.ts @@ -18,4 +18,5 @@ export const indexPatternsMock = ({ return indexPatternMock; } }, -} as unknown) as IndexPatternsService; + updateSavedObject: jest.fn(), +} as unknown) as jest.Mocked; diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx index bba2ef4f8ab85..5e2ef583b00f2 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx @@ -10,7 +10,10 @@ import React from 'react'; import { mountWithIntl } from '@kbn/test/jest'; import { ActionBar, ActionBarProps } from './action_bar'; import { findTestSubject } from '@elastic/eui/lib/test'; -import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants'; +import { + MAX_CONTEXT_SIZE, + MIN_CONTEXT_SIZE, +} from '../../../../components/context_app/utils/constants'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { ['successors', 'predecessors'].forEach((type) => { @@ -31,7 +34,7 @@ describe('Test Discover Context ActionBar for successor | predecessor records', test(`${type}: Load button click`, () => { btn.simulate('click'); - expect(onChangeCount).toHaveBeenCalledWith(25); + expect(onChangeCount).toHaveBeenCalledWith(type, 25); }); test(`${type}: Load button click doesnt submit when MAX_CONTEXT_SIZE was reached`, () => { @@ -44,13 +47,13 @@ describe('Test Discover Context ActionBar for successor | predecessor records', test(`${type}: Count input change submits on blur`, () => { input.simulate('change', { target: { valueAsNumber: 123 } }); input.simulate('blur'); - expect(onChangeCount).toHaveBeenCalledWith(123); + expect(onChangeCount).toHaveBeenCalledWith(type, 123); }); test(`${type}: Count input change submits on return`, () => { input.simulate('change', { target: { valueAsNumber: 124 } }); input.simulate('submit'); - expect(onChangeCount).toHaveBeenCalledWith(124); + expect(onChangeCount).toHaveBeenCalledWith(type, 124); }); test(`${type}: Count input doesnt submits values higher than MAX_CONTEXT_SIZE `, () => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index 93dc4561b4f25..df65312636bb3 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -19,7 +19,10 @@ import { } from '@elastic/eui'; import { ActionBarWarning } from './action_bar_warning'; import { SurrDocType } from '../../api/context'; -import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from '../../query_parameters/constants'; +import { + MAX_CONTEXT_SIZE, + MIN_CONTEXT_SIZE, +} from '../../../../components/context_app/utils/constants'; export interface ActionBarProps { /** 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 deleted file mode 100644 index 8e4fb3ceaef68..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * 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 { getQueryParameterActions } from './actions'; -import { FilterManager, SortDirection } from '../../../../../../data/public'; -import { coreMock } from '../../../../../../../core/public/mocks'; -import { ContextQueryState, LoadingStatus, QueryParameters } from '../../context_query_state'; -import { EsHitRecord } from '../api/context'; -const setupMock = coreMock.createSetup(); - -let state: ContextQueryState; -let filterManager: FilterManager; -let filterManagerSpy: jest.SpyInstance; - -beforeEach(() => { - filterManager = new FilterManager(setupMock.uiSettings); - filterManagerSpy = jest.spyOn(filterManager, 'addFilters'); - - state = { - queryParameters: { - defaultStepSize: 3, - indexPatternId: 'INDEX_PATTERN_ID', - predecessorCount: 10, - successorCount: 10, - anchorId: '', - columns: [], - filters: [], - sort: [['field', SortDirection.asc]], - tieBreakerField: '', - }, - loadingStatus: { - anchor: LoadingStatus.UNINITIALIZED, - predecessors: LoadingStatus.UNINITIALIZED, - successors: LoadingStatus.UNINITIALIZED, - }, - rows: { - all: [], - anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord, - predecessors: [], - successors: [], - }, - useNewFieldsApi: true, - }; -}); - -describe('context query_parameter actions', function () { - describe('action addFilter', () => { - it('should pass the given arguments to the filterManager', () => { - const { addFilter } = getQueryParameterActions(filterManager); - - addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - - // get the generated filter - const generatedFilter = filterManagerSpy.mock.calls[0][0][0]; - const queryKeys = Object.keys(generatedFilter.query.match_phrase); - expect(filterManagerSpy.mock.calls.length).toBe(1); - expect(queryKeys[0]).toBe('FIELD_NAME'); - expect(generatedFilter.query.match_phrase[queryKeys[0]]).toBe('FIELD_VALUE'); - }); - - it('should pass the index pattern id to the filterManager', () => { - const { addFilter } = getQueryParameterActions(filterManager); - addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); - const generatedFilter = filterManagerSpy.mock.calls[0][0][0]; - expect(generatedFilter.meta.index).toBe('INDEX_PATTERN_ID'); - }); - }); - describe('action setPredecessorCount', () => { - it('should set the predecessorCount to the given value', () => { - const { setPredecessorCount } = getQueryParameterActions(filterManager); - setPredecessorCount(state)(20); - expect(state.queryParameters.predecessorCount).toBe(20); - }); - - it('should limit the predecessorCount to 0 as a lower bound', () => { - const { setPredecessorCount } = getQueryParameterActions(filterManager); - setPredecessorCount(state)(-1); - expect(state.queryParameters.predecessorCount).toBe(0); - }); - - it('should limit the predecessorCount to 10000 as an upper bound', () => { - const { setPredecessorCount } = getQueryParameterActions(filterManager); - setPredecessorCount(state)(20000); - expect(state.queryParameters.predecessorCount).toBe(10000); - }); - }); - describe('action setSuccessorCount', () => { - it('should set the successorCount to the given value', function () { - const { setSuccessorCount } = getQueryParameterActions(filterManager); - setSuccessorCount(state)(20); - - expect(state.queryParameters.successorCount).toBe(20); - }); - - it('should limit the successorCount to 0 as a lower bound', () => { - const { setSuccessorCount } = getQueryParameterActions(filterManager); - setSuccessorCount(state)(-1); - expect(state.queryParameters.successorCount).toBe(0); - }); - - it('should limit the successorCount to 10000 as an upper bound', () => { - const { setSuccessorCount } = getQueryParameterActions(filterManager); - setSuccessorCount(state)(20000); - expect(state.queryParameters.successorCount).toBe(10000); - }); - }); - describe('action setQueryParameters', function () { - const { setQueryParameters } = getQueryParameterActions(filterManager); - - it('should update the queryParameters with valid properties from the given object', function () { - const newState = { - ...state, - queryParameters: { - ...state.queryParameters, - additionalParameter: 'ADDITIONAL_PARAMETER', - }, - }; - - const actualState = setQueryParameters(newState)({ - anchorId: 'ANCHOR_ID', - columns: ['column'], - defaultStepSize: 3, - filters: [], - indexPatternId: 'INDEX_PATTERN', - predecessorCount: 100, - successorCount: 100, - sort: [['field', SortDirection.asc]], - tieBreakerField: '', - }); - - expect(actualState).toEqual({ - additionalParameter: 'ADDITIONAL_PARAMETER', - anchorId: 'ANCHOR_ID', - columns: ['column'], - defaultStepSize: 3, - filters: [], - indexPatternId: 'INDEX_PATTERN', - predecessorCount: 100, - successorCount: 100, - sort: [['field', SortDirection.asc]], - tieBreakerField: '', - }); - }); - - it('should ignore invalid properties', function () { - const newState = { ...state }; - - setQueryParameters(newState)(({ - additionalParameter: 'ADDITIONAL_PARAMETER', - } as unknown) as QueryParameters); - - expect(state.queryParameters).toEqual(newState.queryParameters); - }); - }); -}); 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 deleted file mode 100644 index ffaf65e5e3737..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 { pick } from 'lodash'; - -import { - IndexPatternsContract, - FilterManager, - esFilters, - Filter, - IndexPatternField, -} from '../../../../../../data/public'; -import { popularizeField } from '../../../helpers/popularize_field'; -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: ContextQueryState) => (predecessorCount: number) => { - return (state.queryParameters.predecessorCount = clamp( - MIN_CONTEXT_SIZE, - MAX_CONTEXT_SIZE, - predecessorCount - )); - }; - - const setSuccessorCount = (state: ContextQueryState) => (successorCount: number) => { - return (state.queryParameters.successorCount = clamp( - MIN_CONTEXT_SIZE, - MAX_CONTEXT_SIZE, - successorCount - )); - }; - - const setQueryParameters = (state: ContextQueryState) => (queryParameters: QueryParameters) => { - return Object.assign(state.queryParameters, pick(queryParameters, QUERY_PARAMETER_KEYS)); - }; - - const updateFilters = () => (filters: Filter[]) => { - filterManager.setFilters(filters); - }; - - const addFilter = (state: ContextQueryState) => async ( - field: IndexPatternField | string, - values: unknown, - operation: string - ) => { - const indexPatternId = state.queryParameters.indexPatternId; - const newFilters = esFilters.generateFilters( - filterManager, - field, - values, - operation, - indexPatternId - ); - filterManager.addFilters(newFilters); - if (indexPatterns) { - const indexPattern = await indexPatterns.get(indexPatternId); - const fieldName = typeof field === 'string' ? field : field.name; - await popularizeField(indexPattern, fieldName, indexPatterns); - } - }; - - return { - addFilter, - updateFilters, - setPredecessorCount, - setQueryParameters, - setSuccessorCount, - }; -} - -function clamp(minimum: number, maximum: number, value: number) { - return Math.max(Math.min(maximum, value), minimum); -} 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 deleted file mode 100644 index 0bb12f950b82e..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query_parameters/state.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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. - */ - -export function createInitialQueryParametersState( - indexPatternId: string, - anchorId: string, - defaultStepSize: number = 5, - tieBreakerField: string = '_doc' -) { - return { - anchorId, - columns: [], - defaultStepSize, - filters: [], - indexPatternId, - predecessorCount: 5, - successorCount: 5, - sort: [], - tieBreakerField, - }; -} 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 4033ea384688c..d1626d2be4acc 100644 --- a/src/plugins/discover/public/application/angular/context_query_state.ts +++ b/src/plugins/discover/public/application/angular/context_query_state.ts @@ -54,13 +54,6 @@ export interface LoadingStatusEntry { reason?: FailureReason; } -export enum FetchType { - ALL, - CONTEXT, - PREDECESSORS, - SUCCESSORS, -} - export const getInitialContextQueryState = (): ContextFetchState => ({ anchor: {} as EsHitRecord, predecessors: [], diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx index 0d8b98afc5472..e543cd5d9748c 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx @@ -7,61 +7,29 @@ */ import React from 'react'; +import { waitFor } from '@testing-library/react'; import { mountWithIntl } from '@kbn/test/jest'; -import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; +import { createFilterManagerMock } from '../../../../../data/public/query/filter_manager/filter_manager.mock'; import { mockTopNavMenu } from './__mocks__/top_nav_menu'; import { ContextAppContent } from './context_app_content'; import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { ContextApp } from './context_app'; +import { setServices } from '../../../kibana_services'; +import { DiscoverServices } from '../../../build_services'; +import { + CONTEXT_TIE_BREAKER_FIELDS_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from 'src/plugins/discover/common'; +import { indexPatternsMock } from '../../../__mocks__/index_patterns'; +import { act } from 'react-dom/test-utils'; -jest.mock('../../../kibana_services', () => { - return { - getServices: () => ({ - data: { search: { searchSource: { createEmpty: () => {} } } }, - indexPatterns: {}, - capabilities: { - discover: { - save: true, - }, - }, - uiSettings: mockUiSettings, - navigation: { ui: { TopNavMenu: mockTopNavMenu } }, - }), - }; -}); - -jest.mock('./use_context_app_state', () => { - return { - useContextAppState: () => ({ - setAppState: () => {}, - stateContainer: {}, - appState: { sort: [[1603114502000, 2092]] }, - }), - }; -}); - -jest.mock('./use_context_app_fetch', () => { - return { - useContextAppFetch: () => ({ - fetchedState: { - anchor: { _id: 'mock_id', fields: [] }, - predecessors: [], - successors: [], - anchorStatus: { value: 'loaded' }, - predecessorsStatus: { value: 'loaded' }, - successorsStatus: { value: 'loaded' }, - }, - fetchAllRows: () => {}, - fetchContextRows: () => {}, - fetchSurroundingRows: () => {}, - }), - }; -}); +const mockFilterManager = createFilterManagerMock(); +const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } }; describe('ContextApp test', () => { const defaultProps = { indexPattern: indexPatternMock, - indexPatternId: 'mocked_index_pattern', + indexPatternId: 'the-index-pattern-id', anchorId: 'mocked_anchor_id', }; @@ -76,11 +44,68 @@ describe('ContextApp test', () => { useDefaultBehaviors: true, }; - it('renders correctly', () => { + beforeEach(() => { + setServices(({ + data: { + search: { + searchSource: { + createEmpty: jest.fn(), + }, + }, + }, + capabilities: { + discover: { + save: true, + }, + }, + indexPatterns: indexPatternsMock, + toastNotifications: { addDanger: () => {} }, + navigation: mockNavigationPlugin, + core: { notifications: { toasts: [] } }, + history: () => {}, + filterManager: mockFilterManager, + uiSettings: { + get: (key: string) => { + if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) { + return ['_doc']; + } else if (key === SEARCH_FIELDS_FROM_SOURCE) { + return true; + } + }, + }, + } as unknown) as DiscoverServices); + }); + + it('renders correctly', async () => { + const component = mountWithIntl(); + await waitFor(() => { + expect(component.find(ContextAppContent).length).toBe(1); + const topNavMenu = component.find(mockTopNavMenu); + expect(topNavMenu.length).toBe(1); + expect(topNavMenu.props()).toStrictEqual(topNavProps); + }); + }); + + it('should set filters correctly', async () => { const component = mountWithIntl(); - expect(component.find(ContextAppContent).length).toBe(1); - const topNavMenu = component.find(mockTopNavMenu); - expect(topNavMenu.length).toBe(1); - expect(topNavMenu.props()).toStrictEqual(topNavProps); + + await act(async () => { + component.find(ContextAppContent).invoke('addFilter')( + 'message', + '2021-06-08T07:52:19.000Z', + '+' + ); + }); + + expect(mockFilterManager.addFilters.mock.calls.length).toBe(1); + expect(mockFilterManager.addFilters.mock.calls[0][0]).toEqual([ + { + $state: { store: 'appState' }, + meta: { alias: null, disabled: false, index: 'the-index-pattern-id', negate: false }, + query: { match_phrase: { message: '2021-06-08T07:52:19.000Z' } }, + }, + ]); + expect(indexPatternsMock.updateSavedObject.mock.calls.length).toBe(1); + expect(indexPatternsMock.updateSavedObject.mock.calls[0]).toEqual([indexPatternMock, 0, true]); }); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index c15f24a58f4f0..85531253b9dbf 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -24,7 +24,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; -import { SortPairArr } from '../../angular/doc_table/lib/get_sort'; +import { clamp } from './utils/clamp'; export interface ContextAppContentProps { columns: string[]; @@ -149,7 +149,7 @@ export function ContextAppContent({ const onChangeCount = useCallback( (type, count) => { const countKey = type === 'successors' ? 'successorCount' : 'predecessorCount'; - setAppState({ [countKey]: count }); + setAppState({ [countKey]: clamp(count) }); }, [setAppState] ); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 762aad9b5d691..62c2d6f10ea7d 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -13,7 +13,7 @@ import { DiscoverServices } from '../../../build_services'; import { fetchAnchorProvider } from '../../angular/context/api/anchor'; import { EsHitRecord, fetchContextProvider, SurrDocType } from '../../angular/context/api/context'; import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; -import { IndexPattern } from '../../../../../data/public'; +import { IndexPattern, SortDirection } from '../../../../../data/public'; import { ContextFetchState, FailureReason, @@ -143,7 +143,7 @@ export function useContextAppFetch({ anchor as EsHitRecord, sortField, tieBreakerField, - sortDir, + sortDir as SortDirection, count, filters ); diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/index.ts b/src/plugins/discover/public/application/components/context_app/utils/clamp.ts similarity index 63% rename from src/plugins/discover/public/application/angular/context/query_parameters/index.ts rename to src/plugins/discover/public/application/components/context_app/utils/clamp.ts index 2be8d8d9b81e2..0abd03691b2c8 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/index.ts +++ b/src/plugins/discover/public/application/components/context_app/utils/clamp.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ -export { getQueryParameterActions } from './actions'; -export { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, QUERY_PARAMETER_KEYS } from './constants'; -export { createInitialQueryParametersState } from './state'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './constants'; + +export function clamp(value: number) { + return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); +} diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/constants.ts b/src/plugins/discover/public/application/components/context_app/utils/constants.ts similarity index 76% rename from src/plugins/discover/public/application/angular/context/query_parameters/constants.ts rename to src/plugins/discover/public/application/components/context_app/utils/constants.ts index 19b3180d778d7..3ce2c32efd465 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/constants.ts +++ b/src/plugins/discover/public/application/components/context_app/utils/constants.ts @@ -6,8 +6,5 @@ * Side Public License, v 1. */ -import { createInitialQueryParametersState } from './state'; - export const MAX_CONTEXT_SIZE = 10000; // Elasticsearch's default maximum size limit export const MIN_CONTEXT_SIZE = 0; -export const QUERY_PARAMETER_KEYS = Object.keys(createInitialQueryParametersState()); From 54404116091c2e4d0f09153fe0b3e3d0291d0284 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 11:24:14 +0300 Subject: [PATCH 25/36] [Discover] refactor error feedback --- .../context_app/use_context_app_fetch.test.ts | 3 +-- .../context_app/use_context_app_fetch.tsx | 25 +++++++------------ 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts index 2c519e7e41b0c..bc39809acda38 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts @@ -111,7 +111,7 @@ describe('test useContextAppFetch', () => { }); it('should set anchorStatus to failed when tieBreakingField array is empty', async () => { - const { props, dangerNotification } = initDefaults([]); + const { props } = initDefaults([]); const { result } = renderHook(() => { return useContextAppFetch(props); @@ -123,7 +123,6 @@ describe('test useContextAppFetch', () => { await result.current.fetchAllRows(); }); - expect(dangerNotification.mock.calls.length).toBe(1); expect(result.current.fetchedState.anchorStatus.value).toBe(LoadingStatus.FAILED); expect(result.current.fetchedState.anchorStatus.reason).toBe(FailureReason.INVALID_TIEBREAKER); expect(result.current.fetchedState.anchor).toEqual({}); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 62c2d6f10ea7d..44a124956ac7b 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -69,25 +69,12 @@ export function useContextAppFetch({ setFetchedState((prevState) => ({ ...prevState, ...values })); }, []); - const sendInvalidTieBreakerFeedback = useCallback(() => { - const message = i18n.translate('discover.context.invalidTieBreakerFiledSetting', { - defaultMessage: 'Invalid tie breaker field setting', - }); - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), - text: toMountPoint({message}), - }); - }, [toastNotifications]); - const fetchAnchorRow = useCallback(async () => { const { sort } = appState; const [[, sortDir]] = sort; if (!tieBreakerField) { setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER)); - sendInvalidTieBreakerFeedback(); return; } @@ -112,7 +99,6 @@ export function useContextAppFetch({ appState, tieBreakerField, setState, - sendInvalidTieBreakerFeedback, fetchAnchor, indexPatternId, anchorId, @@ -131,7 +117,15 @@ export function useContextAppFetch({ if (!tieBreakerField) { setState(createError(statusKey, FailureReason.INVALID_TIEBREAKER)); - sendInvalidTieBreakerFeedback(); + const message = i18n.translate('discover.context.invalidTieBreakerFiledSetting', { + defaultMessage: 'Invalid tie breaker field setting', + }); + toastNotifications.addDanger({ + title: i18n.translate('discover.context.unableToLoadDocumentDescription', { + defaultMessage: 'Unable to load documents', + }), + text: toMountPoint({message}), + }); return; } @@ -164,7 +158,6 @@ export function useContextAppFetch({ fetchedState.anchor, tieBreakerField, setState, - sendInvalidTieBreakerFeedback, fetchSurroundingDocs, indexPatternId, toastNotifications, From 81f9968e79f7a5925d911b401101d50613f2fd6a Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 11:52:13 +0300 Subject: [PATCH 26/36] [Discover] fix functional test --- .../application/components/context_app/use_context_app_state.ts | 1 + 1 file changed, 1 insertion(+) 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 index 752ab665579d8..688384f223dfe 100644 --- 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 @@ -47,6 +47,7 @@ export function useContextAppState({ useEffect(() => { const unsubscribeAppState = stateContainer.appState.subscribe(async (newState) => { + stateContainer.flushToUrl(true); setState(newState); }); From 1939a4473abeb3316515bef4af894ddbefc10de3 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 19:40:04 +0300 Subject: [PATCH 27/36] [Discover] provide defaultSize --- src/plugins/discover/public/application/angular/context.js | 2 +- .../discover/public/application/angular/context_state.ts | 6 +++--- .../application/components/context_app/context_app.tsx | 6 +----- .../components/context_app/use_context_app_state.ts | 7 +++---- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index b5370ec65d167..43e0c26b168f5 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -23,7 +23,7 @@ const k7Breadcrumbs = () => { }; getAngularModule().config(($routeProvider) => { - $routeProvider.when('/context/:indexPatternId/:id', { + $routeProvider.when('/context/:indexPatternId/:id*', { controller: function ($routeParams, $scope, $route) { this.indexPattern = $route.current.locals.indexPattern.ip; this.anchorId = $routeParams.id; diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 26e2f6e4a1b43..0730ed3369c8d 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -53,7 +53,7 @@ export interface GetStateParams { /** * Number of records to be fetched when 'Load' link/button is clicked */ - defaultStepSize: number; + defaultSize: number; /** * The timefield used for sorting */ @@ -123,7 +123,7 @@ const APP_STATE_URL_KEY = '_a'; * provides helper functions to start/stop syncing with URL */ export function getState({ - defaultStepSize, + defaultSize, timeFieldName, storeInSessionStorage = false, history, @@ -141,7 +141,7 @@ export function getState({ const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; const appStateInitial = createInitialAppState( - defaultStepSize, + defaultSize, timeFieldName, appStateFromUrl, uiSettings diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 68805aec65530..ebec7a69e7fe8 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -49,11 +49,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp /** * Context app state */ - const { appState, setAppState } = useContextAppState({ - indexPattern, - defaultStepSize, - services, - }); + const { appState, setAppState } = useContextAppState({ indexPattern, services }); const prevState = useRef(); /** 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 index 688384f223dfe..e012f3fcec713 100644 --- 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 @@ -8,31 +8,30 @@ import { useEffect, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; +import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../common'; import { IndexPattern } from '../../../../../data/public'; import { DiscoverServices } from '../../../build_services'; import { AppState, getState } from '../../angular/context_state'; export function useContextAppState({ indexPattern, - defaultStepSize, services, }: { indexPattern: IndexPattern; - defaultStepSize: number; services: DiscoverServices; }) { const { uiSettings: config, history, core, filterManager } = services; const stateContainer = useMemo(() => { return getState({ - defaultStepSize, + defaultSize: config.get(CONTEXT_DEFAULT_SIZE_SETTING), timeFieldName: indexPattern.timeFieldName as string, storeInSessionStorage: config.get('state:storeInSessionStorage'), history: history(), toasts: core.notifications.toasts, uiSettings: config, }); - }, [defaultStepSize, config, history, indexPattern, core.notifications.toasts]); + }, [config, history, indexPattern, core.notifications.toasts]); const [appState, setState] = useState(stateContainer.appState.getState()); From 67b1bc164766d8b3afbe22d36fe532058453da75 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 20:05:07 +0300 Subject: [PATCH 28/36] [Discover] clean up code --- .../discover/public/__mocks__/ui_settings.ts | 12 +------ ...text_app_content.scss => context_app.scss} | 0 .../components/context_app/context_app.tsx | 1 + .../context_app/context_app_content.tsx | 1 - .../context_app/use_context_app_fetch.tsx | 33 +++++++++---------- 5 files changed, 17 insertions(+), 30 deletions(-) rename src/plugins/discover/public/application/components/context_app/{context_app_content.scss => context_app.scss} (100%) diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index 789eff9635efd..8347ff18edd7d 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,13 +7,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { - CONTEXT_DEFAULT_SIZE_SETTING, - DEFAULT_COLUMNS_SETTING, - DOC_TABLE_LEGACY, - SAMPLE_SIZE_SETTING, - SEARCH_FIELDS_FROM_SOURCE, -} from '../../common'; +import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -23,10 +17,6 @@ export const uiSettingsMock = ({ return ['default_column']; } else if (key === DOC_TABLE_LEGACY) { return true; - } else if (key === SEARCH_FIELDS_FROM_SOURCE) { - return false; - } else if (key === CONTEXT_DEFAULT_SIZE_SETTING) { - return 5; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.scss b/src/plugins/discover/public/application/components/context_app/context_app.scss similarity index 100% rename from src/plugins/discover/public/application/components/context_app/context_app_content.scss rename to src/plugins/discover/public/application/components/context_app/context_app.scss diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index ebec7a69e7fe8..88b3b9aa69b1d 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -7,6 +7,7 @@ */ import React, { Fragment, memo, useEffect, useRef, useMemo, useCallback } from 'react'; +import './context_app.scss'; import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 85531253b9dbf..d7331625cde70 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -7,7 +7,6 @@ */ import React, { useState, Fragment, useMemo, useCallback } from 'react'; -import './context_app_content.scss'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 44a124956ac7b..11173766de6cc 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -72,9 +72,22 @@ export function useContextAppFetch({ const fetchAnchorRow = useCallback(async () => { const { sort } = appState; const [[, sortDir]] = sort; + const anchorError = i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + defaultMessage: 'Unable to load the anchor document', + }); if (!tieBreakerField) { setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER)); + toastNotifications.addDanger({ + title: anchorError, + text: toMountPoint( + + {i18n.translate('discover.context.invalidTieBreakerFiledSetting', { + defaultMessage: 'Invalid tie breaker field setting', + })} + + ), + }); return; } @@ -88,9 +101,7 @@ export function useContextAppFetch({ return anchor; } catch (error) { toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { - defaultMessage: 'Unable to load the anchor document', - }), + title: anchorError, text: toMountPoint({error.message}), }); setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); @@ -99,10 +110,10 @@ export function useContextAppFetch({ appState, tieBreakerField, setState, + toastNotifications, fetchAnchor, indexPatternId, anchorId, - toastNotifications, ]); const fetchSurroundingRows = useCallback( @@ -115,20 +126,6 @@ export function useContextAppFetch({ const anchor = fetchedAnchor || fetchedState.anchor; const statusKey = `${type}Status`; - if (!tieBreakerField) { - setState(createError(statusKey, FailureReason.INVALID_TIEBREAKER)); - const message = i18n.translate('discover.context.invalidTieBreakerFiledSetting', { - defaultMessage: 'Invalid tie breaker field setting', - }); - toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), - text: toMountPoint({message}), - }); - return; - } - try { setState({ [statusKey]: { value: LoadingStatus.LOADING } }); const rows = await fetchSurroundingDocs( From 057e030f16b73b35cfe12bd0821516245f2410ff Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 20:06:26 +0300 Subject: [PATCH 29/36] [Discover] fix eslint error --- .../discover/public/application/angular/context_state.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index a22ce479c6d78..ad4051a0c97fe 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -23,7 +23,7 @@ describe('Test Discover Context State', () => { history = createBrowserHistory(); history.push('/'); state = getState({ - defaultStepSize: 4, + defaultSize: 4, timeFieldName: 'time', history, uiSettings: { From 2ac1b8ad5935928b8092bc48a1debe3ae265e8ef Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Tue, 8 Jun 2021 22:33:33 +0300 Subject: [PATCH 30/36] [Discover] fiix context settings --- .../application/components/context_app/context_app.tsx | 10 +--------- .../components/context_app/context_app_content.tsx | 5 ++--- .../components/context_app/use_context_app_state.ts | 2 +- 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 88b3b9aa69b1d..078609c8c4121 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -13,11 +13,7 @@ import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { esFilters, SortDirection } from '../../../../../data/public'; -import { - CONTEXT_DEFAULT_SIZE_SETTING, - DOC_TABLE_LEGACY, - SEARCH_FIELDS_FROM_SOURCE, -} from '../../../../common'; +import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; import { LoadingStatus } from '../../angular/context_query_state'; @@ -43,9 +39,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp const isLegacy = useMemo(() => config.get(DOC_TABLE_LEGACY), [config]); const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); - const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), [ - config, - ]); /** * Context app state @@ -167,7 +160,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp anchorStatus={fetchedState.anchorStatus.value} predecessorsStatus={fetchedState.predecessorsStatus.value} successorsStatus={fetchedState.successorsStatus.value} - defaultStepSize={defaultStepSize} useNewFieldsApi={useNewFieldsApi} isLegacy={isLegacy} setAppState={setAppState} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index d7331625cde70..087b911e25071 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -9,7 +9,7 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; -import { DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; +import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; import { SortDirection } from '../../../../../data/public'; import { @@ -42,7 +42,6 @@ export interface ContextAppContentProps { predecessorsStatus: LoadingStatus; successorsStatus: LoadingStatus; useNewFieldsApi: boolean; - defaultStepSize: number; isLegacy: boolean; setAppState: (newState: Partial) => void; addFilter: ( @@ -70,7 +69,6 @@ export function ContextAppContent({ anchorStatus, predecessorsStatus, successorsStatus, - defaultStepSize, useNewFieldsApi, isLegacy, setAppState, @@ -91,6 +89,7 @@ export function ContextAppContent({ () => !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, [config, indexPattern] ); + const defaultStepSize = useMemo(() => parseInt(config.get(CONTEXT_STEP_SETTING), 10), [config]); const docTableProps = () => { return { 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 index e012f3fcec713..ddf8817270258 100644 --- 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 @@ -24,7 +24,7 @@ export function useContextAppState({ const stateContainer = useMemo(() => { return getState({ - defaultSize: config.get(CONTEXT_DEFAULT_SIZE_SETTING), + defaultSize: parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), timeFieldName: indexPattern.timeFieldName as string, storeInSessionStorage: config.get('state:storeInSessionStorage'), history: history(), From 49a5ed617b3e890413ecbbf161f8fe809136a769 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 9 Jun 2021 00:10:16 +0300 Subject: [PATCH 31/36] [Discover] return tieBreaker field check --- .../context_app/use_context_app_fetch.tsx | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 11173766de6cc..436607dc50a92 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -72,14 +72,14 @@ export function useContextAppFetch({ const fetchAnchorRow = useCallback(async () => { const { sort } = appState; const [[, sortDir]] = sort; - const anchorError = i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { + const errorTitle = i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', }); if (!tieBreakerField) { setState(createError('anchorStatus', FailureReason.INVALID_TIEBREAKER)); toastNotifications.addDanger({ - title: anchorError, + title: errorTitle, text: toMountPoint( {i18n.translate('discover.context.invalidTieBreakerFiledSetting', { @@ -100,11 +100,11 @@ export function useContextAppFetch({ setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } }); return anchor; } catch (error) { + setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ - title: anchorError, + title: errorTitle, text: toMountPoint({error.message}), }); - setState(createError('anchorStatus', FailureReason.UNKNOWN, error)); } }, [ appState, @@ -125,6 +125,24 @@ export function useContextAppFetch({ const count = type === 'predecessors' ? appState.predecessorCount : appState.successorCount; const anchor = fetchedAnchor || fetchedState.anchor; const statusKey = `${type}Status`; + const errorTitle = i18n.translate('discover.context.unableToLoadDocumentDescription', { + defaultMessage: 'Unable to load documents', + }); + + if (!tieBreakerField) { + setState(createError(statusKey, FailureReason.INVALID_TIEBREAKER)); + toastNotifications.addDanger({ + title: errorTitle, + text: toMountPoint( + + {i18n.translate('discover.context.invalidTieBreakerFiledSetting', { + defaultMessage: 'Invalid tie breaker field setting', + })} + + ), + }); + return; + } try { setState({ [statusKey]: { value: LoadingStatus.LOADING } }); @@ -140,13 +158,11 @@ export function useContextAppFetch({ ); setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } }); } catch (error) { + setState(createError(statusKey, FailureReason.UNKNOWN, error)); toastNotifications.addDanger({ - title: i18n.translate('discover.context.unableToLoadDocumentDescription', { - defaultMessage: 'Unable to load documents', - }), + title: errorTitle, text: toMountPoint({error.message}), }); - setState(createError(statusKey, FailureReason.UNKNOWN, error)); } }, [ From 105d04a3ea3119dd65eb1b1ab6531e2c18e1ef9a Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 9 Jun 2021 20:03:17 +0300 Subject: [PATCH 32/36] [Discover] optimize things --- .../components/context_app/context_app.tsx | 13 ++++++++----- .../components/context_app/use_context_app_state.ts | 4 ++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 078609c8c4121..2379168f29ccc 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -86,11 +86,14 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp useNewFieldsApi, setAppState, }); - const rows = [ - ...(fetchedState.predecessors || []), - ...(fetchedState.anchor._id ? [fetchedState.anchor] : []), - ...(fetchedState.successors || []), - ]; + const rows = useMemo( + () => [ + ...(fetchedState.predecessors || []), + ...(fetchedState.anchor._id ? [fetchedState.anchor] : []), + ...(fetchedState.successors || []), + ], + [fetchedState] + ); const addFilter = useCallback( async (field: IndexPatternField | string, values: unknown, operation: string) => { 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 index ddf8817270258..83cd73ee57927 100644 --- 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 @@ -45,9 +45,9 @@ export function useContextAppState({ }, [stateContainer]); useEffect(() => { - const unsubscribeAppState = stateContainer.appState.subscribe(async (newState) => { + const unsubscribeAppState = stateContainer.appState.subscribe((newState) => { stateContainer.flushToUrl(true); - setState(newState); + setState((prevState) => ({ ...prevState, ...newState })); }); return () => unsubscribeAppState(); From 2b53b3250c503e5efddcdfbbe38f0d59b2dcedb7 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 10 Jun 2021 18:42:53 +0300 Subject: [PATCH 33/36] [Discover] optimize rerenders --- .../components/context_app/context_app.tsx | 14 +++++++------- .../context_app/context_app_content.tsx | 17 +++++++++++------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 2379168f29ccc..1e2b2eb722434 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -44,7 +44,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp * Context app state */ const { appState, setAppState } = useContextAppState({ indexPattern, services }); - const prevState = useRef(); + const prevAppState = useRef(); /** * Context fetched state @@ -64,17 +64,17 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp * Fetch docs on ui changes */ useEffect(() => { - if (!prevState.current) { + if (!prevAppState.current) { fetchAllRows(); - } else if (prevState.current.predecessorCount !== appState.predecessorCount) { + } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { fetchSurroundingRows('predecessors'); - } else if (prevState.current.successorCount !== appState.successorCount) { + } else if (prevAppState.current.successorCount !== appState.successorCount) { fetchSurroundingRows('successors'); - } else if (!isEqualFilters(prevState.current.filters, appState.filters)) { + } else if (!isEqualFilters(prevAppState.current.filters, appState.filters)) { fetchContextRows(); } - prevState.current = cloneDeep(appState); + prevAppState.current = cloneDeep(appState); }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ @@ -92,7 +92,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp ...(fetchedState.anchor._id ? [fetchedState.anchor] : []), ...(fetchedState.successors || []), ], - [fetchedState] + [fetchedState.predecessors, fetchedState.anchor, fetchedState.successors] ); const addFilter = useCallback( diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 087b911e25071..1df530c361d67 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -51,7 +51,11 @@ export interface ContextAppContentProps { ) => Promise; } +const controlColumnIds = ['openDetails']; + const DataGridMemoized = React.memo(DiscoverGrid); +const DocTableLegacyMemoized = React.memo(DocTableLegacy); +const ActionBarMemoized = React.memo(ActionBar); export function ContextAppContent({ columns, @@ -78,7 +82,8 @@ export function ContextAppContent({ const [expandedDoc, setExpandedDoc] = useState(undefined); const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; - const isAnchorLoading = anchorStatus === LoadingStatus.LOADING; + const isAnchorLoading = + anchorStatus === LoadingStatus.LOADING || anchorStatus === LoadingStatus.UNINITIALIZED; const arePredecessorsLoading = predecessorsStatus === LoadingStatus.LOADING || predecessorsStatus === LoadingStatus.UNINITIALIZED; @@ -106,7 +111,7 @@ export function ContextAppContent({ services, useNewFieldsApi, isPaginationEnabled: false, - controlColumnIds: ['openDetails'], + controlColumnIds, setExpandedDoc, onFilter: addFilter, onAddColumn, @@ -125,7 +130,7 @@ export function ContextAppContent({ onFilter: addFilter, onAddColumn, onRemoveColumn, - sort: sort.map((el) => [el]), + sort, useNewFieldsApi, } as DocTableLegacyProps; }; @@ -154,7 +159,7 @@ export function ContextAppContent({ return ( - {isLegacy && isAnchorLoaded && (
- +
)} {!isLegacy && ( @@ -176,7 +181,7 @@ export function ContextAppContent({
)} - Date: Thu, 10 Jun 2021 19:00:47 +0300 Subject: [PATCH 34/36] Update src/plugins/discover/public/application/components/context_app/context_app.tsx Co-authored-by: Matthias Wilhelm --- .../public/application/components/context_app/context_app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 1e2b2eb722434..2803eb8313bd0 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -15,7 +15,7 @@ import { cloneDeep } from 'lodash'; import { esFilters, SortDirection } from '../../../../../data/public'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { IndexPattern, IndexPatternField } from '../../../../../data/common'; import { LoadingStatus } from '../../angular/context_query_state'; import { getServices } from '../../../kibana_services'; import { AppState, isEqualFilters } from '../../angular/context_state'; From 19dc507a58479db50fde7239980b9294ef83163e Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Thu, 10 Jun 2021 19:06:27 +0300 Subject: [PATCH 35/36] [Discover] resolve comments --- .../context_app/context_app_content.tsx | 8 ++++-- .../context_app/use_context_app_fetch.test.ts | 27 ------------------- .../context_app/use_context_app_fetch.tsx | 15 ----------- .../context_app/use_context_app_state.ts | 2 +- .../components/context_app/utils/clamp.ts | 13 --------- 5 files changed, 7 insertions(+), 58 deletions(-) delete mode 100644 src/plugins/discover/public/application/components/context_app/utils/clamp.ts diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 1df530c361d67..3d6e6874eb119 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -10,7 +10,7 @@ import React, { useState, Fragment, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHorizontalRule, EuiText } from '@elastic/eui'; import { CONTEXT_STEP_SETTING, DOC_HIDE_TIME_COLUMN_SETTING } from '../../../../common'; -import { IndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { IndexPattern, IndexPatternField } from '../../../../../data/common'; import { SortDirection } from '../../../../../data/public'; import { DocTableLegacy, @@ -23,7 +23,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; -import { clamp } from './utils/clamp'; +import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; export interface ContextAppContentProps { columns: string[]; @@ -53,6 +53,10 @@ export interface ContextAppContentProps { const controlColumnIds = ['openDetails']; +export function clamp(value: number) { + return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); +} + const DataGridMemoized = React.memo(DiscoverGrid); const DocTableLegacyMemoized = React.memo(DocTableLegacy); const ActionBarMemoized = React.memo(ActionBar); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts index bc39809acda38..7d537f44fffdf 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.test.ts @@ -171,33 +171,6 @@ describe('test useContextAppFetch', () => { expect(result.current.fetchedState.successors).toEqual(mockSuccessorHits); }); - it('should set context rows statuses to failed when tieBreakingField array is empty', async () => { - const { props, dangerNotification } = initDefaults([]); - - const { result } = renderHook(() => { - return useContextAppFetch(props); - }); - - expect(result.current.fetchedState.predecessorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); - expect(result.current.fetchedState.successorsStatus.value).toBe(LoadingStatus.UNINITIALIZED); - - await act(async () => { - await result.current.fetchContextRows(mockAnchorHit); - }); - - expect(dangerNotification.mock.calls.length).toBe(2); // for successors and predecessors - expect(result.current.fetchedState.predecessorsStatus).toEqual({ - value: LoadingStatus.FAILED, - reason: FailureReason.INVALID_TIEBREAKER, - }); - expect(result.current.fetchedState.successorsStatus).toEqual({ - value: LoadingStatus.FAILED, - reason: FailureReason.INVALID_TIEBREAKER, - }); - expect(result.current.fetchedState.predecessors).toEqual([]); - expect(result.current.fetchedState.successors).toEqual([]); - }); - it('should set context rows statuses to failed when invalid indexPatternId provided', async () => { const { props, dangerNotification } = initDefaults(['_doc'], ''); diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 436607dc50a92..55e4ceb26f436 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -129,21 +129,6 @@ export function useContextAppFetch({ defaultMessage: 'Unable to load documents', }); - if (!tieBreakerField) { - setState(createError(statusKey, FailureReason.INVALID_TIEBREAKER)); - toastNotifications.addDanger({ - title: errorTitle, - text: toMountPoint( - - {i18n.translate('discover.context.invalidTieBreakerFiledSetting', { - defaultMessage: 'Invalid tie breaker field setting', - })} - - ), - }); - return; - } - try { setState({ [statusKey]: { value: LoadingStatus.LOADING } }); const rows = await fetchSurroundingDocs( 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 index 83cd73ee57927..94c4f9c45a1e3 100644 --- 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 @@ -25,7 +25,7 @@ export function useContextAppState({ const stateContainer = useMemo(() => { return getState({ defaultSize: parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), - timeFieldName: indexPattern.timeFieldName as string, + timeFieldName: indexPattern.timeFieldName!, storeInSessionStorage: config.get('state:storeInSessionStorage'), history: history(), toasts: core.notifications.toasts, diff --git a/src/plugins/discover/public/application/components/context_app/utils/clamp.ts b/src/plugins/discover/public/application/components/context_app/utils/clamp.ts deleted file mode 100644 index 0abd03691b2c8..0000000000000 --- a/src/plugins/discover/public/application/components/context_app/utils/clamp.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * 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 { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './constants'; - -export function clamp(value: number) { - return Math.max(Math.min(MAX_CONTEXT_SIZE, value), MIN_CONTEXT_SIZE); -} From 19e6840aac3362e7b3894639d8c7f6ec268f0256 Mon Sep 17 00:00:00 2001 From: Dzmitry Tamashevich Date: Wed, 16 Jun 2021 14:21:55 +0300 Subject: [PATCH 36/36] [Discover] replace url instead of pushing to history. refactoring --- .../discover/public/__mocks__/ui_settings.ts | 12 +++++++++- .../public/application/angular/context.html | 3 ++- .../context/api/context.predecessors.test.ts | 6 ++--- .../context/api/context.successors.test.ts | 6 ++--- .../angular/context/api/context.ts | 12 +++++++--- .../context/api/utils/generate_intervals.ts | 7 +++--- .../api/utils/get_es_query_search_after.ts | 3 ++- .../components/action_bar/action_bar.test.tsx | 5 ++-- .../components/action_bar/action_bar.tsx | 5 ++-- .../action_bar/action_bar_warning.tsx | 2 +- .../application/angular/context_state.ts | 2 +- .../context_app/context_app.test.tsx | 15 ++---------- .../components/context_app/context_app.tsx | 19 ++++++++------- .../context_app/context_app_content.tsx | 10 ++++---- .../context_app/use_context_app_fetch.test.ts | 24 +++++++++++-------- .../context_app/use_context_app_fetch.tsx | 7 +++--- .../context_app/use_context_app_state.ts | 1 - 17 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index 8347ff18edd7d..9f05338e04572 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,7 +7,13 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; +import { + CONTEXT_TIE_BREAKER_FIELDS_SETTING, + DEFAULT_COLUMNS_SETTING, + DOC_TABLE_LEGACY, + SAMPLE_SIZE_SETTING, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -17,6 +23,10 @@ export const uiSettingsMock = ({ return ['default_column']; } else if (key === DOC_TABLE_LEGACY) { return true; + } else if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) { + return ['_doc']; + } else if (key === SEARCH_FIELDS_FROM_SOURCE) { + return false; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index b5254721a1e9d..6cb5088f66605 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -1,4 +1,5 @@ \ No newline at end of file + anchor-id="contextAppRoute.anchorId"> + diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index 1acf57411c795..ca74c77676edb 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -9,7 +9,7 @@ import moment from 'moment'; import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { EsHitRecordList, fetchContextProvider } from './context'; +import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; import { EsHitRecord } from './context'; import { Query } from '../../../../../../data/public'; @@ -73,7 +73,7 @@ describe('context app', function () { }; return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( - 'predecessors', + SurrDocType.PREDECESSORS, indexPatternId, anchor as EsHitRecord, timeField, @@ -265,7 +265,7 @@ describe('context app', function () { }; return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( - 'predecessors', + SurrDocType.PREDECESSORS, indexPatternId, anchor as EsHitRecord, timeField, diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index 957a13e8daf09..ba61dd15af46b 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -12,7 +12,7 @@ import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; -import { EsHitRecordList, fetchContextProvider } from './context'; +import { EsHitRecordList, fetchContextProvider, SurrDocType } from './context'; import { EsHitRecord } from './context'; import { DiscoverServices } from '../../../../build_services'; @@ -73,7 +73,7 @@ describe('context app', function () { }; return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( - 'successors', + SurrDocType.SUCCESSORS, indexPatternId, anchor as EsHitRecord, timeField, @@ -268,7 +268,7 @@ describe('context app', function () { }; return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( - 'successors', + SurrDocType.SUCCESSORS, indexPatternId, anchor as EsHitRecord, timeField, 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..727f941f5fee3 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -16,7 +16,11 @@ import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; -export type SurrDocType = 'successors' | 'predecessors'; +export enum SurrDocType { + SUCCESSORS = 'successors', + PREDECESSORS = 'predecessors', +} + export type EsHitRecord = Required< Pick< estypes.SearchResponse['hits']['hits'][number], @@ -68,7 +72,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields } const indexPattern = await indexPatterns.get(indexPatternId); const searchSource = await createSearchSource(indexPattern, filters); - const sortDirToApply = type === 'successors' ? sortDir : reverseSortDir(sortDir); + const sortDirToApply = type === SurrDocType.SUCCESSORS ? sortDir : reverseSortDir(sortDir); const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor.fields[timeField][0]) : ''; const timeValueMillis = @@ -108,7 +112,9 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields ); documents = - type === 'successors' ? [...documents, ...hits] : [...hits.slice().reverse(), ...documents]; + type === SurrDocType.SUCCESSORS + ? [...documents, ...hits] + : [...hits.slice().reverse(), ...documents]; } return documents; diff --git a/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts b/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts index b1a54116ee296..47952f4f84759 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/generate_intervals.ts @@ -7,6 +7,7 @@ */ import { SortDirection } from '../../../../../../../data/public'; +import { SurrDocType } from '../context'; export type IntervalValue = number | null; @@ -31,12 +32,12 @@ export function* asPairs(iterable: Iterable): IterableIterator { const offsetSign = - (sort === SortDirection.asc && type === 'successors') || - (sort === SortDirection.desc && type === 'predecessors') + (sort === SortDirection.asc && type === SurrDocType.SUCCESSORS) || + (sort === SortDirection.desc && type === SurrDocType.PREDECESSORS) ? 1 : -1; // ending with `null` opens the last interval diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index c703abaf2e523..c8064202f2c82 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -26,7 +26,8 @@ export function getEsQuerySearchAfter( ): EsQuerySearchAfter { if (documents.length) { // already surrounding docs -> first or last record is used - const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; + const afterTimeRecIdx = + type === SurrDocType.SUCCESSORS && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; let afterTimeValue = afterTimeDoc.sort[0] as string | number; if (nanoSeconds) { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx index 5e2ef583b00f2..357eecb29f4a2 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.test.tsx @@ -14,9 +14,10 @@ import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, } from '../../../../components/context_app/utils/constants'; +import { SurrDocType } from '../../api/context'; describe('Test Discover Context ActionBar for successor | predecessor records', () => { - ['successors', 'predecessors'].forEach((type) => { + [SurrDocType.SUCCESSORS, SurrDocType.PREDECESSORS].forEach((type) => { const onChangeCount = jest.fn(); const props = { defaultStepSize: 5, @@ -71,7 +72,7 @@ describe('Test Discover Context ActionBar for successor | predecessor records', }); test(`${type}: Warning about limitation of additional records`, () => { - if (type === 'predecessors') { + if (type === SurrDocType.PREDECESSORS) { expect(findTestSubject(wrapper, 'predecessorsWarningMsg').text()).toBe( 'No documents newer than the anchor could be found.' ); diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx index df65312636bb3..843e24b189824 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar.tsx @@ -48,9 +48,10 @@ export interface ActionBarProps { isLoading: boolean; /** * is triggered when the input containing count is changed + * @param type * @param count */ - onChangeCount: (type: string, count: number) => void; + onChangeCount: (type: SurrDocType, count: number) => void; /** * can be `predecessors` or `successors`, usage in context: * predecessors action bar + records (these are newer records) @@ -70,7 +71,7 @@ export function ActionBar({ type, }: ActionBarProps) { const showWarning = !isDisabled && !isLoading && docCountAvailable < docCount; - const isSuccessor = type === 'successors'; + const isSuccessor = type === SurrDocType.SUCCESSORS; const [newDocCount, setNewDocCount] = useState(docCount); const isValid = (value: number) => value >= MIN_CONTEXT_SIZE && value <= MAX_CONTEXT_SIZE; const onSubmit = (ev: React.FormEvent) => { diff --git a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx index e64bf25fe755b..2d189ad4c9b9c 100644 --- a/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx +++ b/src/plugins/discover/public/application/angular/context/components/action_bar/action_bar_warning.tsx @@ -12,7 +12,7 @@ import { EuiCallOut } from '@elastic/eui'; import { SurrDocType } from '../../api/context'; export function ActionBarWarning({ docCount, type }: { docCount: number; type: SurrDocType }) { - if (type === 'predecessors') { + if (type === SurrDocType.PREDECESSORS) { return ( [ diff --git a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx index e543cd5d9748c..7ac6a9d0e8de3 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.test.tsx @@ -16,12 +16,9 @@ import { indexPatternMock } from '../../../__mocks__/index_pattern'; import { ContextApp } from './context_app'; import { setServices } from '../../../kibana_services'; import { DiscoverServices } from '../../../build_services'; -import { - CONTEXT_TIE_BREAKER_FIELDS_SETTING, - SEARCH_FIELDS_FROM_SOURCE, -} from 'src/plugins/discover/common'; import { indexPatternsMock } from '../../../__mocks__/index_patterns'; import { act } from 'react-dom/test-utils'; +import { uiSettingsMock } from '../../../__mocks__/ui_settings'; const mockFilterManager = createFilterManagerMock(); const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } }; @@ -64,15 +61,7 @@ describe('ContextApp test', () => { core: { notifications: { toasts: [] } }, history: () => {}, filterManager: mockFilterManager, - uiSettings: { - get: (key: string) => { - if (key === CONTEXT_TIE_BREAKER_FIELDS_SETTING) { - return ['_doc']; - } else if (key === SEARCH_FIELDS_FROM_SOURCE) { - return true; - } - }, - }, + uiSettings: uiSettingsMock, } as unknown) as DiscoverServices); }); diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 2803eb8313bd0..c52f22c60bb5b 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -24,6 +24,7 @@ import { useContextAppState } from './use_context_app_state'; import { useContextAppFetch } from './use_context_app_fetch'; import { popularizeField } from '../../helpers/popularize_field'; import { ContextAppContent } from './context_app_content'; +import { SurrDocType } from '../../angular/context/api/context'; const ContextAppContentMemoized = memo(ContextAppContent); @@ -67,9 +68,9 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp if (!prevAppState.current) { fetchAllRows(); } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { - fetchSurroundingRows('predecessors'); + fetchSurroundingRows(SurrDocType.PREDECESSORS); } else if (prevAppState.current.successorCount !== appState.successorCount) { - fetchSurroundingRows('successors'); + fetchSurroundingRows(SurrDocType.SUCCESSORS); } else if (!isEqualFilters(prevAppState.current.filters, appState.filters)) { fetchContextRows(); } @@ -148,25 +149,25 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp
diff --git a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx index 3d6e6874eb119..4d7ce2aa52092 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_content.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_content.tsx @@ -21,7 +21,7 @@ import { ActionBar } from '../../angular/context/components/action_bar/action_ba import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { AppState } from '../../angular/context_state'; -import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; +import { EsHitRecord, EsHitRecordList, SurrDocType } from '../../angular/context/api/context'; import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; @@ -154,8 +154,8 @@ export function ContextAppContent({ }; const onChangeCount = useCallback( - (type, count) => { - const countKey = type === 'successors' ? 'successorCount' : 'predecessorCount'; + (type: SurrDocType, count: number) => { + const countKey = type === SurrDocType.SUCCESSORS ? 'successorCount' : 'predecessorCount'; setAppState({ [countKey]: clamp(count) }); }, [setAppState] @@ -164,7 +164,7 @@ export function ContextAppContent({ return ( ({ - fetchContextProvider: () => ({ - fetchSurroundingDocs: (type: string, indexPatternId: string) => { - if (!indexPatternId) { - throw new Error(); - } - return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; - }, - }), -})); +jest.mock('../../angular/context/api/context', () => { + const originalModule = jest.requireActual('../../angular/context/api/context'); + return { + ...originalModule, + fetchContextProvider: () => ({ + fetchSurroundingDocs: (type: string, indexPatternId: string) => { + if (!indexPatternId) { + throw new Error(); + } + return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; + }, + }), + }; +}); jest.mock('../../angular/context/api/anchor', () => ({ fetchAnchorProvider: () => (indexPatternId: string) => { diff --git a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx index 55e4ceb26f436..5bb23eae3e2e2 100644 --- a/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/components/context_app/use_context_app_fetch.tsx @@ -122,7 +122,8 @@ export function useContextAppFetch({ const { sort } = appState; const [[sortField, sortDir]] = sort; - const count = type === 'predecessors' ? appState.predecessorCount : appState.successorCount; + const count = + type === SurrDocType.PREDECESSORS ? appState.predecessorCount : appState.successorCount; const anchor = fetchedAnchor || fetchedState.anchor; const statusKey = `${type}Status`; const errorTitle = i18n.translate('discover.context.unableToLoadDocumentDescription', { @@ -165,8 +166,8 @@ export function useContextAppFetch({ const fetchContextRows = useCallback( (anchor?: EsHitRecord) => Promise.allSettled([ - fetchSurroundingRows('predecessors', anchor), - fetchSurroundingRows('successors', anchor), + fetchSurroundingRows(SurrDocType.PREDECESSORS, anchor), + fetchSurroundingRows(SurrDocType.SUCCESSORS, anchor), ]), [fetchSurroundingRows] ); 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 index 94c4f9c45a1e3..a2d64b0ecde07 100644 --- 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 @@ -46,7 +46,6 @@ export function useContextAppState({ useEffect(() => { const unsubscribeAppState = stateContainer.appState.subscribe((newState) => { - stateContainer.flushToUrl(true); setState((prevState) => ({ ...prevState, ...newState })); });