From c06ba9252ad65d283ef5ceacfaa6989abf29332f Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Thu, 6 May 2021 15:59:54 +0300 Subject: [PATCH] [Discover] Migrate remaining context files from js to ts (#99019) (#99457) * [Discover] migrate remaining context files from js to ts * [Discover] get rid of any types * [Discover] replace constants with enums, update imports * [Discover] use unknown instead of any, correct types * [Discover] skip any type for tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../context/api/{_stubs.js => _stubs.ts} | 68 +++++----- .../api/{anchor.test.js => anchor.test.ts} | 73 ++++++----- .../context/api/{anchor.js => anchor.ts} | 33 ++++- ...s.test.js => context.predecessors.test.ts} | 117 ++++++++++-------- ...ors.test.js => context.successors.test.ts} | 104 +++++++++------- .../angular/context/api/context.ts | 7 +- .../context/query/{actions.js => actions.tsx} | 90 +++++++++----- .../context/query/{index.js => index.ts} | 1 - .../context/query/{state.js => state.ts} | 10 +- .../context/query_parameters/actions.js | 64 ---------- .../context/query_parameters/actions.test.ts | 46 ++++--- .../context/query_parameters/actions.ts | 82 ++++++++++++ .../query_parameters/{index.js => index.ts} | 0 .../public/application/angular/context_app.js | 12 +- .../application/angular/context_app_state.ts | 60 +++++++++ .../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 +- 20 files changed, 477 insertions(+), 341 deletions(-) rename src/plugins/discover/public/application/angular/context/api/{_stubs.js => _stubs.ts} (54%) rename src/plugins/discover/public/application/angular/context/api/{anchor.test.js => anchor.test.ts} (73%) rename src/plugins/discover/public/application/angular/context/api/{anchor.js => anchor.ts} (61%) rename src/plugins/discover/public/application/angular/context/api/{context.predecessors.test.js => context.predecessors.test.ts} (77%) rename src/plugins/discover/public/application/angular/context/api/{context.successors.test.js => context.successors.test.ts} (79%) rename src/plugins/discover/public/application/angular/context/query/{actions.js => actions.tsx} (63%) rename src/plugins/discover/public/application/angular/context/query/{index.js => index.ts} (83%) rename src/plugins/discover/public/application/angular/context/query/{state.js => state.ts} (57%) delete mode 100644 src/plugins/discover/public/application/angular/context/query_parameters/actions.js create mode 100644 src/plugins/discover/public/application/angular/context/query_parameters/actions.ts 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 delete mode 100644 src/plugins/discover/public/application/components/context_app/constants.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 54% 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..241d0a621f245 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,17 @@ import sinon from 'sinon'; import moment from 'moment'; +import { IndexPatternsContract } from '../../../../../../data/public'; +import { EsHitRecordList } from './context'; + +type SortHit = { + [key in string]: number; // timeField name +} & { + sort: [number, number]; +}; + export function createIndexPatternsStub() { - return { + return ({ get: sinon.spy((indexPatternId) => Promise.resolve({ id: indexPatternId, @@ -18,62 +27,59 @@ 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: EsHitRecordList, timeField?: string) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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(timeFieldName: string) { + const searchSourceStub = createSearchSourceStub([], timeFieldName); searchSourceStub.fetch = sinon.spy(() => { - const timeField = 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, second) => first[timeField] - second[timeField] - : (first, second) => 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) => + (hit: SortHit) => moment(hit[timeField]).isSameOrAfter(timeRange.gte) && moment(hit[timeField]).isSameOrBefore(timeRange.lte) ) @@ -87,5 +93,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 73% 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..62c9a2a5e3b90 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,31 @@ * Side Public License, v 1. */ +import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; - -import { fetchAnchorProvider } from './anchor'; +import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; describe('context app', function () { - describe('function fetchAnchor', function () { - let fetchAnchor; - let searchSourceStub; + let fetchAnchor: ( + indexPatternId: string, + anchorId: string, + sort: EsQuerySortValue[] + ) => Promise; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + 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 +38,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 +49,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 +59,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 +70,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 +81,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 +103,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 +119,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 +135,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,11 +145,10 @@ describe('context app', function () { }); describe('useNewFields API', () => { - let fetchAnchor; - let searchSourceStub; - beforeEach(() => { - searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }]); + searchSourceStub = createSearchSourceStub([ + { _id: 'hit1', fields: [], sort: [], _source: {} }, + ]); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); @@ -147,8 +156,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 61% 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..da81ce525331a 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,31 @@ * Side Public License, v 1. */ -import _ from 'lodash'; +import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi = false) { - return async function fetchAnchor(indexPatternId, anchorId, sort) { +import { + ISearchSource, + IndexPatternsContract, + EsQuerySortValue, +} 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: EsQuerySortValue[] + ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) @@ -36,7 +56,7 @@ export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi } 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.', @@ -45,8 +65,9 @@ export function fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi } 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/api/context.predecessors.test.js b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts similarity index 77% 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..dc097bc110e20 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,11 @@ 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'; +import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; const ANCHOR_TIMESTAMP = new Date(MS_PER_DAY).toJSON(); @@ -18,15 +21,31 @@ 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockSearchSource: any; + describe('function fetchPredecessors', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp', MS_PER_DAY * 8); + mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -34,7 +53,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchPredecessors = ( indexPatternId, @@ -44,7 +63,7 @@ describe('context app', function () { timeValNr, tieBreakerField, tieBreakerValue, - size + size = 10 ) => { const anchor = { _source: { @@ -56,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -78,14 +97,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 +121,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 +140,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 +158,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 +184,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 +199,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 +216,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,13 +234,10 @@ 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({ + setServices(({ data: { search: { searchSource: { @@ -235,7 +245,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchPredecessors = ( indexPatternId, @@ -245,7 +255,7 @@ describe('context app', function () { timeValNr, tieBreakerField, tieBreakerValue, - size + size = 10 ) => { const anchor = { _source: { @@ -257,7 +267,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -279,14 +289,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 79% 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..f8fc7eb343206 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,24 +10,42 @@ 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'; +import { DiscoverServices } from '../../../../build_services'; 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; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let mockSearchSource: any; + describe('function fetchSuccessors', function () { beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -35,7 +53,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchSuccessors = ( indexPatternId, @@ -57,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'successors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -79,13 +97,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 +121,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 +140,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 +160,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 +187,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 +202,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 +219,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,13 +237,10 @@ describe('context app', function () { }); describe('function fetchSuccessors with useNewFieldsApi set', function () { - let fetchSuccessors; - let mockSearchSource; - beforeEach(() => { - mockSearchSource = createContextSearchSourceStub([], '@timestamp'); + mockSearchSource = createContextSearchSourceStub('@timestamp'); - setServices({ + setServices(({ data: { search: { searchSource: { @@ -239,7 +248,7 @@ describe('context app', function () { }, }, }, - }); + } as unknown) as DiscoverServices); fetchSuccessors = ( indexPatternId, @@ -261,7 +270,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'successors', indexPatternId, - anchor, + anchor as AnchorHitRecord, timeField, tieBreakerField, sortDir, @@ -283,13 +292,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 0316178862aa9..4309b9ca4c391 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 { @@ -41,7 +42,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 @@ -52,13 +53,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 63% 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..52c56d379d259 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.js +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -6,19 +6,30 @@ * Side Public License, v 1. */ -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; import React from 'react'; -import { getServices } from '../../../../kibana_services'; +import { fromPairs } from 'lodash'; +import { i18n } from '@kbn/i18n'; -import { fetchAnchorProvider } from '../api/anchor'; -import { fetchContextProvider } from '../api/context'; -import { getQueryParameterActions } from '../query_parameters'; -import { FAILURE_REASONS, LOADING_STATUS } from './index'; -import { MarkdownSimple } from '../../../../../../kibana_react/public'; +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 { + 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 +43,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 +71,7 @@ export function QueryActionsProvider(Promise) { if (!tieBreakerField) { return Promise.reject( setFailedStatus(state)('anchor', { - reason: FAILURE_REASONS.INVALID_TIEBREAKER, + reason: FailureReason.INVALID_TIEBREAKER, }) ); } @@ -65,27 +79,27 @@ export function QueryActionsProvider(Promise) { setLoadingStatus(state)('anchor'); return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [_.fromPairs([sort]), { [tieBreakerField]: sort[1] }]) + 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: toMountPoint({error.message}), }); throw error; } ); }; - const fetchSurroundingRows = (type, state) => { + const fetchSurroundingRows = (type: SurrDocType, state: ContextAppState) => { const { queryParameters: { indexPatternId, sort, tieBreakerField }, rows: { anchor }, @@ -100,7 +114,7 @@ export function QueryActionsProvider(Promise) { if (!tieBreakerField) { return Promise.reject( setFailedStatus(state)(type, { - reason: FAILURE_REASONS.INVALID_TIEBREAKER, + reason: FailureReason.INVALID_TIEBREAKER, }) ); } @@ -120,54 +134,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: toMountPoint({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 83% rename from src/plugins/discover/public/application/angular/context/query/index.js rename to src/plugins/discover/public/application/angular/context/query/index.ts index a718cdb237774..70e3dc1600472 100644 --- a/src/plugins/discover/public/application/angular/context/query/index.js +++ 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/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.js b/src/plugins/discover/public/application/angular/context/query_parameters/actions.js deleted file mode 100644 index b308196cb8b04..0000000000000 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.js +++ /dev/null @@ -1,64 +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 { esFilters } from '../../../../../../data/public'; -import { popularizeField } from '../../../helpers/popularize_field'; - -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( - MIN_CONTEXT_SIZE, - MAX_CONTEXT_SIZE, - predecessorCount - )); - - const setSuccessorCount = (state) => (successorCount) => - (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 updateFilters = () => (filters) => { - filterManager.setFilters(filters); - }; - - const addFilter = (state) => async (field, values, operation) => { - 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); - await popularizeField(indexPattern, field.name, indexPatterns); - } - }; - - return { - addFilter, - updateFilters, - setPredecessorCount, - setQueryParameters, - setSuccessorCount, - }; -} - -function clamp(minimum, maximum, value) { - return Math.max(Math.min(maximum, value), minimum); -} 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 efadf2105074e..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,20 +6,13 @@ * Side Public License, v 1. */ -// @ts-expect-error 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 +26,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 +116,7 @@ describe('context query_parameter actions', function () { const newState = { ...state, queryParameters: { + ...state.queryParameters, additionalParameter: 'ADDITIONAL_PARAMETER', }, }; @@ -113,11 +125,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 +138,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.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts new file mode 100644 index 0000000000000..02eab422b68a4 --- /dev/null +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.ts @@ -0,0 +1,82 @@ +/* + * 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 { ContextAppState, QueryParameters } from '../../context_app_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) => { + return (state.queryParameters.predecessorCount = clamp( + MIN_CONTEXT_SIZE, + MAX_CONTEXT_SIZE, + predecessorCount + )); + }; + + const setSuccessorCount = (state: ContextAppState) => (successorCount: number) => { + return (state.queryParameters.successorCount = clamp( + MIN_CONTEXT_SIZE, + MAX_CONTEXT_SIZE, + successorCount + )); + }; + + const setQueryParameters = (state: ContextAppState) => (queryParameters: QueryParameters) => { + return Object.assign(state.queryParameters, pick(queryParameters, QUERY_PARAMETER_KEYS)); + }; + + const updateFilters = () => (filters: Filter[]) => { + filterManager.setFilters(filters); + }; + + const addFilter = (state: ContextAppState) => 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/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.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_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; +} diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index 8ac111a8fe087..d60f2e655c4eb 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 cd3571f447cf5..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-expect-error -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 37791e0350ef7..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-expect-error -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 && (