diff --git a/src/core_plugins/kbn_doc_views/public/views/table.html b/src/core_plugins/kbn_doc_views/public/views/table.html index d98f5be7512a5a8..7de7695318d35e1 100644 --- a/src/core_plugins/kbn_doc_views/public/views/table.html +++ b/src/core_plugins/kbn_doc_views/public/views/table.html @@ -1,6 +1,6 @@ - +
{ expect(searchSourceStub.fetch.calledOnce).to.be(true); }); }); it('should configure the SearchSource to not inherit from the implicit root', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const inheritsSpy = searchSourceStub.inherits; expect(inheritsSpy.calledOnce).to.be(true); @@ -46,22 +48,20 @@ describe('context app', function () { }); it('should set the SearchSource index pattern', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const setIndexSpy = searchSourceStub.set.withArgs('index'); expect(setIndexSpy.calledOnce).to.be(true); - expect(setIndexSpy.firstCall.args[1]).to.eql(indexPatternStub); + expect(setIndexSpy.firstCall.args[1]).to.eql({ id: 'INDEX_PATTERN_ID' }); }); }); it('should set the SearchSource version flag to true', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const setVersionSpy = searchSourceStub.set.withArgs('version'); expect(setVersionSpy.calledOnce).to.be(true); @@ -70,10 +70,9 @@ describe('context app', function () { }); it('should set the SearchSource size to 1', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const setSizeSpy = searchSourceStub.set.withArgs('size'); expect(setSizeSpy.calledOnce).to.be(true); @@ -82,10 +81,9 @@ describe('context app', function () { }); it('should set the SearchSource query to a _uid terms query', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const setQuerySpy = searchSourceStub.set.withArgs('query'); expect(setQuerySpy.calledOnce).to.be(true); @@ -98,10 +96,9 @@ describe('context app', function () { }); it('should set the SearchSource sort order', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then(() => { const setSortSpy = searchSourceStub.set.withArgs('sort'); expect(setSortSpy.calledOnce).to.be(true); @@ -113,11 +110,10 @@ describe('context app', function () { }); it('should reject with an error when no hits were found', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); searchSourceStub._stubHits = []; - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then( () => { expect().fail('expected the promise to be rejected'); @@ -129,14 +125,13 @@ describe('context app', function () { }); it('should return the first hit after adding an anchor marker', function () { - const indexPatternStub = createIndexPatternStub('index1'); const searchSourceStub = new SearchSourceStub(); searchSourceStub._stubHits = [ { property1: 'value1' }, { property2: 'value2' }, ]; - return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' }) + return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' }) .then((anchorDocument) => { expect(anchorDocument).to.have.property('property1', 'value1'); expect(anchorDocument).to.have.property('$$_isAnchor', true); @@ -146,12 +141,13 @@ describe('context app', function () { }); -function createIndexPatternStub(indices) { +function createCourierStub() { return { - getComputedFields: sinon.stub() - .returns({}), - toIndexList: sinon.stub() - .returns(indices), + indexPatterns: { + get: sinon.spy((indexPatternId) => Promise.resolve({ + id: indexPatternId, + })), + }, }; } @@ -160,6 +156,7 @@ function createSearchSourceStubProvider(hits) { _stubHits: hits, }; + searchSourceStub.filter = sinon.stub().returns(searchSourceStub); searchSourceStub.inherits = sinon.stub().returns(searchSourceStub); searchSourceStub.set = sinon.stub().returns(searchSourceStub); searchSourceStub.fetch = sinon.spy(() => Promise.resolve({ diff --git a/src/core_plugins/kibana/public/context/api/anchor.js b/src/core_plugins/kibana/public/context/api/anchor.js index 4c395473cba4bc4..66c6784674753cb 100644 --- a/src/core_plugins/kibana/public/context/api/anchor.js +++ b/src/core_plugins/kibana/public/context/api/anchor.js @@ -3,10 +3,12 @@ import _ from 'lodash'; import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; -function fetchAnchorProvider(Private) { +function fetchAnchorProvider(courier, Private) { const SearchSource = Private(SearchSourceProvider); - return async function fetchAnchor(indexPattern, uid, sort) { + return async function fetchAnchor(indexPatternId, uid, sort) { + const indexPattern = await courier.indexPatterns.get(indexPatternId); + const searchSource = new SearchSource() .inherits(false) .set('index', indexPattern) diff --git a/src/core_plugins/kibana/public/context/api/context.js b/src/core_plugins/kibana/public/context/api/context.js index 808457566291bc6..bd5d678d768dd8e 100644 --- a/src/core_plugins/kibana/public/context/api/context.js +++ b/src/core_plugins/kibana/public/context/api/context.js @@ -5,7 +5,7 @@ import { SearchSourceProvider } from 'ui/courier/data_source/search_source'; import { reverseSortDirective } from './utils/sorting'; -function fetchContextProvider(Private) { +function fetchContextProvider(courier, Private) { const SearchSource = Private(SearchSourceProvider); return { @@ -13,37 +13,43 @@ function fetchContextProvider(Private) { fetchSuccessors, }; - async function fetchSuccessors(indexPattern, anchorDocument, contextSort, size) { + async function fetchSuccessors(indexPatternId, anchorDocument, contextSort, size, filters) { const successorsSort = [contextSort, { _uid: 'asc' }]; - const successorsSearchSource = createSearchSource( - indexPattern, + const successorsSearchSource = await createSearchSource( + indexPatternId, anchorDocument, successorsSort, size, + filters, ); const results = await performQuery(successorsSearchSource); return results; } - async function fetchPredecessors(indexPattern, anchorDocument, contextSort, size) { + async function fetchPredecessors(indexPatternId, anchorDocument, contextSort, size, filters) { const predecessorsSort = [reverseSortDirective(contextSort), { _uid: 'desc' }]; - const predecessorsSearchSource = createSearchSource( - indexPattern, + const predecessorsSearchSource = await createSearchSource( + indexPatternId, anchorDocument, predecessorsSort, size, + filters, ); const reversedResults = await performQuery(predecessorsSearchSource); const results = reversedResults.slice().reverse(); return results; } - function createSearchSource(indexPattern, anchorDocument, sort, size) { + async function createSearchSource(indexPatternId, anchorDocument, sort, size, filters) { + + const indexPattern = await courier.indexPatterns.get(indexPatternId); + return new SearchSource() .inherits(false) .set('index', indexPattern) .set('version', true) .set('size', size) + .set('filter', filters) .set('query', { match_all: {}, }) diff --git a/src/core_plugins/kibana/public/context/app.html b/src/core_plugins/kibana/public/context/app.html index 5a0b193ebc972d7..fef98142f1ce2df 100644 --- a/src/core_plugins/kibana/public/context/app.html +++ b/src/core_plugins/kibana/public/context/app.html @@ -2,7 +2,7 @@
- Surrounding Documents in {{ contextApp.state.queryParameters.indexPattern.id }} + Surrounding Documents in {{ contextApp.state.queryParameters.indexPatternId }}
@@ -13,6 +13,8 @@ + +
diff --git a/src/core_plugins/kibana/public/context/index.js b/src/core_plugins/kibana/public/context/index.js index 07f8f750bd30d59..cc9c6cb36f7d773 100644 --- a/src/core_plugins/kibana/public/context/index.js +++ b/src/core_plugins/kibana/public/context/index.js @@ -1,3 +1,6 @@ +import _ from 'lodash'; + +import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; import uiRoutes from 'ui/routes'; import './app'; @@ -6,12 +9,12 @@ import contextAppRouteTemplate from './index.html'; uiRoutes -.when('/context/:indexPattern/:type/:id', { +.when('/context/:indexPatternId/:type/:id', { controller: ContextAppRouteController, controllerAs: 'contextAppRoute', resolve: { indexPattern: function ($route, courier) { - return courier.indexPatterns.get($route.current.params.indexPattern); + return courier.indexPatterns.get($route.current.params.indexPatternId); }, }, template: contextAppRouteTemplate, @@ -25,8 +28,11 @@ function ContextAppRouteController( chrome, config, indexPattern, + Private, ) { - this.state = new AppState(createDefaultAppState(config)); + const queryFilter = Private(FilterBarQueryFilterProvider); + + this.state = new AppState(createDefaultAppState(config, indexPattern)); this.state.save(true); $scope.$watchGroup([ @@ -34,15 +40,23 @@ function ContextAppRouteController( 'contextAppRoute.state.predecessorCount', 'contextAppRoute.state.successorCount', ], () => this.state.save(true)); + + $scope.$listen(queryFilter, 'update', () => { + this.filters = _.cloneDeep(queryFilter.getFilters()); + }); + this.anchorUid = getDocumentUid($routeParams.type, $routeParams.id); this.indexPattern = indexPattern; this.discoverUrl = chrome.getNavLinkById('kibana:discover').lastSubUrl; + this.filters = _.cloneDeep(queryFilter.getFilters()); } -function createDefaultAppState(config) { +function createDefaultAppState(config, indexPattern) { return { columns: ['_source'], + filters: [], predecessorCount: parseInt(config.get('context:defaultSize'), 10), + sort: [indexPattern.timeFieldName, 'desc'], successorCount: parseInt(config.get('context:defaultSize'), 10), }; } diff --git a/src/core_plugins/kibana/public/context/query/actions.js b/src/core_plugins/kibana/public/context/query/actions.js index 72ffc1be64a9dee..e05b6c00c75c682 100644 --- a/src/core_plugins/kibana/public/context/query/actions.js +++ b/src/core_plugins/kibana/public/context/query/actions.js @@ -6,7 +6,7 @@ import { QueryParameterActionsProvider } from '../query_parameters'; import { LOADING_STATUS } from './constants'; -export function QueryActionsProvider(es, Notifier, Private, Promise) { +export function QueryActionsProvider(courier, Notifier, Private, Promise) { const fetchAnchor = Private(fetchAnchorProvider); const { fetchPredecessors, fetchSuccessors } = Private(fetchContextProvider); const { @@ -26,12 +26,12 @@ export function QueryActionsProvider(es, Notifier, Private, Promise) { ); const fetchAnchorRow = (state) => () => { - const { queryParameters: { indexPattern, anchorUid, sort } } = state; + const { queryParameters: { indexPatternId, anchorUid, sort } } = state; setLoadingStatus(state)('anchor', LOADING_STATUS.LOADING); return Promise.try(() => ( - fetchAnchor(indexPattern, anchorUid, _.zipObject([sort])) + fetchAnchor(indexPatternId, anchorUid, _.zipObject([sort])) )) .then( (anchorDocument) => { @@ -49,14 +49,14 @@ export function QueryActionsProvider(es, Notifier, Private, Promise) { const fetchPredecessorRows = (state) => () => { const { - queryParameters: { indexPattern, predecessorCount, sort }, + queryParameters: { indexPatternId, filters, predecessorCount, sort }, rows: { anchor }, } = state; setLoadingStatus(state)('predecessors', LOADING_STATUS.LOADING); return Promise.try(() => ( - fetchPredecessors(indexPattern, anchor, _.zipObject([sort]), predecessorCount) + fetchPredecessors(indexPatternId, anchor, _.zipObject([sort]), predecessorCount, filters) )) .then( (predecessorDocuments) => { @@ -74,14 +74,14 @@ export function QueryActionsProvider(es, Notifier, Private, Promise) { const fetchSuccessorRows = (state) => () => { const { - queryParameters: { indexPattern, sort, successorCount }, + queryParameters: { indexPatternId, filters, sort, successorCount }, rows: { anchor }, } = state; setLoadingStatus(state)('successors', LOADING_STATUS.LOADING); return Promise.try(() => ( - fetchSuccessors(indexPattern, anchor, _.zipObject([sort]), successorCount) + fetchSuccessors(indexPatternId, anchor, _.zipObject([sort]), successorCount, filters) )) .then( (successorDocuments) => { @@ -97,14 +97,23 @@ export function QueryActionsProvider(es, Notifier, Private, Promise) { ); }; + const fetchContextRows = (state) => () => ( + Promise.all([ + fetchPredecessorRows(state)(), + fetchSuccessorRows(state)(), + ]) + ); + const fetchAllRows = (state) => () => ( Promise.try(fetchAnchorRow(state)) - .then(() => Promise.all([ - fetchPredecessorRows(state)(), - fetchSuccessorRows(state)(), - ])) + .then(fetchContextRows(state)) ); + const fetchContextRowsWithNewQueryParameters = (state) => (queryParameters) => { + setQueryParameters(state)(queryParameters); + return fetchContextRows(state)(); + }; + const fetchAllRowsWithNewQueryParameters = (state) => (queryParameters) => { setQueryParameters(state)(queryParameters); return fetchAllRows(state)(); @@ -142,6 +151,8 @@ export function QueryActionsProvider(es, Notifier, Private, Promise) { fetchAllRows, fetchAllRowsWithNewQueryParameters, fetchAnchorRow, + fetchContextRows, + fetchContextRowsWithNewQueryParameters, fetchGivenPredecessorRows, fetchGivenSuccessorRows, fetchMorePredecessorRows, diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js index 70328e9fc4fa3d2..73044c011df692e 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/_utils.js @@ -5,6 +5,7 @@ export function createStateStub(overrides) { return _.merge({ queryParameters: { defaultStepSize: 3, + indexPatternId: 'INDEX_PATTERN_ID', predecessorCount: 10, successorCount: 10, }, diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js new file mode 100644 index 000000000000000..b0b75f613511a3e --- /dev/null +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js @@ -0,0 +1,53 @@ +import expect from 'expect.js'; +import ngMock from 'ng_mock'; +import sinon from 'sinon'; + +import { FilterManagerProvider } from 'ui/filter_manager'; + +import { createStateStub } from './_utils'; +import { QueryParameterActionsProvider } from '../actions'; + + +describe('context app', function () { + beforeEach(ngMock.module('kibana')); + + describe('action addFilter', function () { + let filterManagerStub; + let addFilter; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + filterManagerStub = createFilterManagerStub(); + Private.stub(FilterManagerProvider, filterManagerStub); + + addFilter = Private(QueryParameterActionsProvider).addFilter; + })); + + it('should pass the given arguments to the filterManager', function () { + const state = createStateStub(); + + addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); + + const filterManagerAddStub = filterManagerStub.add; + expect(filterManagerAddStub.calledOnce).to.be(true); + expect(filterManagerAddStub.firstCall.args[0]).to.eql('FIELD_NAME'); + expect(filterManagerAddStub.firstCall.args[1]).to.eql('FIELD_VALUE'); + expect(filterManagerAddStub.firstCall.args[2]).to.eql('FILTER_OPERATION'); + }); + + it('should pass the index pattern id to the filterManager', function () { + const state = createStateStub(); + + addFilter(state)('FIELD_NAME', 'FIELD_VALUE', 'FILTER_OPERATION'); + + const filterManagerAddStub = filterManagerStub.add; + expect(filterManagerAddStub.calledOnce).to.be(true); + expect(filterManagerAddStub.firstCall.args[3]).to.eql('INDEX_PATTERN_ID'); + }); + }); +}); + +function createFilterManagerStub() { + return { + add: sinon.stub(), + }; +} diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js index 0f787515f1839d0..6006bdaf4840059 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_predecessor_count.js @@ -1,13 +1,25 @@ import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import { FilterManagerProvider } from 'ui/filter_manager'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; describe('context app', function () { + beforeEach(ngMock.module('kibana')); + describe('action increasePredecessorCount', function () { + let increasePredecessorCount; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + Private.stub(FilterManagerProvider, {}); + + increasePredecessorCount = Private(QueryParameterActionsProvider).increasePredecessorCount; + })); + it('should increase the predecessorCount by the given value', function () { - const { increasePredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increasePredecessorCount(state)(20); @@ -16,7 +28,6 @@ describe('context app', function () { }); it('should increase the predecessorCount by the default step size if not value is given', function () { - const { increasePredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increasePredecessorCount(state)(); @@ -25,7 +36,6 @@ describe('context app', function () { }); it('should limit the predecessorCount to 0 as a lower bound', function () { - const { increasePredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increasePredecessorCount(state)(-20); @@ -34,7 +44,6 @@ describe('context app', function () { }); it('should limit the predecessorCount to 10000 as an upper bound', function () { - const { increasePredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increasePredecessorCount(state)(20000); diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js index efa25bbe167c73b..c9b7b9870d0efb5 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_increase_successor_count.js @@ -1,13 +1,25 @@ import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import { FilterManagerProvider } from 'ui/filter_manager'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; describe('context app', function () { + beforeEach(ngMock.module('kibana')); + describe('action increaseSuccessorCount', function () { + let increaseSuccessorCount; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + Private.stub(FilterManagerProvider, {}); + + increaseSuccessorCount = Private(QueryParameterActionsProvider).increaseSuccessorCount; + })); + it('should increase the successorCount by the given value', function () { - const { increaseSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increaseSuccessorCount(state)(20); @@ -16,7 +28,6 @@ describe('context app', function () { }); it('should increase the successorCount by the default step size if not value is given', function () { - const { increaseSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increaseSuccessorCount(state)(); @@ -25,7 +36,6 @@ describe('context app', function () { }); it('should limit the successorCount to 0 as a lower bound', function () { - const { increaseSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increaseSuccessorCount(state)(-20); @@ -34,7 +44,6 @@ describe('context app', function () { }); it('should limit the successorCount to 10000 as an upper bound', function () { - const { increaseSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); increaseSuccessorCount(state)(20000); diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js index 6d80dce42fe9c84..321ea5d0ab1b06b 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_predecessor_count.js @@ -1,13 +1,25 @@ import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import { FilterManagerProvider } from 'ui/filter_manager'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; describe('context app', function () { + beforeEach(ngMock.module('kibana')); + describe('action setPredecessorCount', function () { + let setPredecessorCount; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + Private.stub(FilterManagerProvider, {}); + + setPredecessorCount = Private(QueryParameterActionsProvider).setPredecessorCount; + })); + it('should set the predecessorCount to the given value', function () { - const { setPredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setPredecessorCount(state)(20); @@ -16,7 +28,6 @@ describe('context app', function () { }); it('should limit the predecessorCount to 0 as a lower bound', function () { - const { setPredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setPredecessorCount(state)(-1); @@ -25,7 +36,6 @@ describe('context app', function () { }); it('should limit the predecessorCount to 10000 as an upper bound', function () { - const { setPredecessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setPredecessorCount(state)(20000); diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js index 7b010a512132ed5..f1b05278a6e976c 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_query_parameters.js @@ -1,13 +1,25 @@ import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import { FilterManagerProvider } from 'ui/filter_manager'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; describe('context app', function () { + beforeEach(ngMock.module('kibana')); + describe('action setQueryParameters', function () { + let setQueryParameters; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + Private.stub(FilterManagerProvider, {}); + + setQueryParameters = Private(QueryParameterActionsProvider).setQueryParameters; + })); + it('should update the queryParameters with valid properties from the given object', function () { - const { setQueryParameters } = new QueryParameterActionsProvider(); const state = createStateStub({ queryParameters: { additionalParameter: 'ADDITIONAL_PARAMETER', @@ -18,7 +30,8 @@ describe('context app', function () { anchorUid: 'ANCHOR_UID', columns: ['column'], defaultStepSize: 3, - indexPattern: 'INDEX_PATTERN', + filters: ['filter'], + indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, sort: ['field'], @@ -29,7 +42,8 @@ describe('context app', function () { anchorUid: 'ANCHOR_UID', columns: ['column'], defaultStepSize: 3, - indexPattern: 'INDEX_PATTERN', + filters: ['filter'], + indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, sort: ['field'], @@ -37,7 +51,6 @@ describe('context app', function () { }); it('should ignore invalid properties', function () { - const { setQueryParameters } = new QueryParameterActionsProvider(); const state = createStateStub(); setQueryParameters(state)({ diff --git a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js index 4e3744708e093d8..be6f14795456f22 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js +++ b/src/core_plugins/kibana/public/context/query_parameters/__tests__/action_set_successor_count.js @@ -1,13 +1,25 @@ import expect from 'expect.js'; +import ngMock from 'ng_mock'; + +import { FilterManagerProvider } from 'ui/filter_manager'; import { createStateStub } from './_utils'; import { QueryParameterActionsProvider } from '../actions'; describe('context app', function () { + beforeEach(ngMock.module('kibana')); + describe('action setSuccessorCount', function () { + let setSuccessorCount; + + beforeEach(ngMock.inject(function createPrivateStubs(Private) { + Private.stub(FilterManagerProvider, {}); + + setSuccessorCount = Private(QueryParameterActionsProvider).setSuccessorCount; + })); + it('should set the successorCount to the given value', function () { - const { setSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setSuccessorCount(state)(20); @@ -16,7 +28,6 @@ describe('context app', function () { }); it('should limit the successorCount to 0 as a lower bound', function () { - const { setSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setSuccessorCount(state)(-1); @@ -25,7 +36,6 @@ describe('context app', function () { }); it('should limit the successorCount to 10000 as an upper bound', function () { - const { setSuccessorCount } = new QueryParameterActionsProvider(); const state = createStateStub(); setSuccessorCount(state)(20000); diff --git a/src/core_plugins/kibana/public/context/query_parameters/actions.js b/src/core_plugins/kibana/public/context/query_parameters/actions.js index 6a838cd1374ecbd..b6c9c4a73b01b30 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/actions.js +++ b/src/core_plugins/kibana/public/context/query_parameters/actions.js @@ -1,5 +1,6 @@ import _ from 'lodash'; +import { FilterManagerProvider } from 'ui/filter_manager'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE, @@ -7,7 +8,9 @@ import { } from './constants'; -export function QueryParameterActionsProvider() { +export function QueryParameterActionsProvider(courier, Private) { + const filterManager = Private(FilterManagerProvider); + const setPredecessorCount = (state) => (predecessorCount) => ( state.queryParameters.predecessorCount = clamp( MIN_CONTEXT_SIZE, @@ -43,7 +46,15 @@ export function QueryParameterActionsProvider() { ) ); + const addFilter = (state) => async (field, values, operation) => { + const indexPatternId = state.queryParameters.indexPatternId; + filterManager.add(field, values, operation, indexPatternId); + const indexPattern = await courier.indexPatterns.get(indexPatternId); + indexPattern.popularizeField(field.name, 1); + }; + return { + addFilter, increasePredecessorCount, increaseSuccessorCount, setPredecessorCount, diff --git a/src/core_plugins/kibana/public/context/query_parameters/state.js b/src/core_plugins/kibana/public/context/query_parameters/state.js index 4f8a98d8d797ea2..5b9023e209a30b5 100644 --- a/src/core_plugins/kibana/public/context/query_parameters/state.js +++ b/src/core_plugins/kibana/public/context/query_parameters/state.js @@ -3,7 +3,8 @@ export function createInitialQueryParametersState(defaultStepSize) { anchorUid: null, columns: [], defaultStepSize, - indexPattern: null, + filters: [], + indexPatternId: null, predecessorCount: 0, successorCount: 0, sort: [], diff --git a/src/core_plugins/kibana/public/discover/index.html b/src/core_plugins/kibana/public/discover/index.html index 80d89a7f9dc3d0f..fc0dc6c81804fc3 100644 --- a/src/core_plugins/kibana/public/discover/index.html +++ b/src/core_plugins/kibana/public/discover/index.html @@ -144,6 +144,7 @@

Searching

columns="state.columns" infinite-scroll="true" filter="filterQuery" + filters="state.filters" data-shared-item data-title="{{opts.savedSearch.lastSavedTitle}}" data-description="{{opts.savedSearch.description}}" diff --git a/src/ui/public/doc_table/components/table_row.js b/src/ui/public/doc_table/components/table_row.js index f474e35011adad1..6ba4827162311d1 100644 --- a/src/ui/public/doc_table/components/table_row.js +++ b/src/ui/public/doc_table/components/table_row.js @@ -11,6 +11,8 @@ import { noWhiteSpace } from 'ui/utils/no_white_space'; import openRowHtml from 'ui/doc_table/components/table_row/open.html'; import detailsHtml from 'ui/doc_table/components/table_row/details.html'; import { uiModules } from 'ui/modules'; +import { disableFilter } from 'ui/filter_bar'; + const module = uiModules.get('app/discover'); @@ -35,6 +37,7 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl scope: { columns: '=', filter: '=', + filters: '=?', indexPattern: '=', row: '=kbnTableRow', onAddColumn: '=?', @@ -102,6 +105,7 @@ module.directive('kbnTableRow', function ($compile, $httpParamSerializer, kbnUrl const hash = $httpParamSerializer({ _a: rison.encode({ columns: $scope.columns, + filters: ($scope.filters || []).map(disableFilter), }), }); return `${path}?${hash}`; diff --git a/src/ui/public/doc_table/doc_table.html b/src/ui/public/doc_table/doc_table.html index 96b9ab144768a5e..bab9514e1d2acf6 100644 --- a/src/ui/public/doc_table/doc_table.html +++ b/src/ui/public/doc_table/doc_table.html @@ -43,6 +43,7 @@ sorting="sorting" index-pattern="indexPattern" filter="filter" + filters="filters" class="discover-table-row" on-add-column="onAddColumn" on-change-sort-order="onChangeSortOrder" @@ -92,6 +93,7 @@ sorting="sorting" index-pattern="indexPattern" filter="filter" + filters="filters" class="discover-table-row" ng-class="{'discover-table-row--highlight': row['$$_isAnchor']}" data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}" diff --git a/src/ui/public/doc_table/doc_table.js b/src/ui/public/doc_table/doc_table.js index 98589f5aa906960..be11e2a729b063e 100644 --- a/src/ui/public/doc_table/doc_table.js +++ b/src/ui/public/doc_table/doc_table.js @@ -23,6 +23,7 @@ uiModules.get('kibana') searchSource: '=?', infiniteScroll: '=?', filter: '=?', + filters: '=?', onAddColumn: '=?', onChangeSortOrder: '=?', onMoveColumn: '=?', diff --git a/src/ui/public/filter_bar/filter_bar.html b/src/ui/public/filter_bar/filter_bar.html index ca5ba6fa4bf1dcb..4598d28cddba403 100644 --- a/src/ui/public/filter_bar/filter_bar.html +++ b/src/ui/public/filter_bar/filter_bar.html @@ -23,7 +23,12 @@
-
+
{{ filter.meta.alias }} diff --git a/src/ui/public/filter_bar/filter_bar.js b/src/ui/public/filter_bar/filter_bar.js index a0de162c95a2c42..b9aed3f269c6b2c 100644 --- a/src/ui/public/filter_bar/filter_bar.js +++ b/src/ui/public/filter_bar/filter_bar.js @@ -10,8 +10,11 @@ import { FilterBarLibChangeTimeFilterProvider } from 'ui/filter_bar/lib/change_t import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter'; import { compareFilters } from './lib/compare_filters'; import { uiModules } from 'ui/modules'; -const module = uiModules.get('kibana'); +export { disableFilter, enableFilter, toggleFilterDisabled } from './lib/disable_filter'; + + +const module = uiModules.get('kibana'); module.directive('filterBar', function (Private, Promise, getAppState) { const mapAndFlattenFilters = Private(FilterBarLibMapAndFlattenFiltersProvider); diff --git a/src/ui/public/filter_bar/lib/__tests__/disable_filter.js b/src/ui/public/filter_bar/lib/__tests__/disable_filter.js new file mode 100644 index 000000000000000..0d0a2faa60c99af --- /dev/null +++ b/src/ui/public/filter_bar/lib/__tests__/disable_filter.js @@ -0,0 +1,121 @@ +import expect from 'expect.js'; + +import { + disableFilter, + enableFilter, + toggleFilterDisabled, +} from '../disable_filter'; + + +describe('function disableFilter', function () { + it('should disable a filter that is explicitly enabled', function () { + const enabledFilter = { + meta: { + disabled: false, + }, + match_all: {}, + }; + + expect(disableFilter(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should disable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(disableFilter(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const disabledFilter = disableFilter(enabledFilterWithProperties); + expect(disabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(disabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); + +describe('function enableFilter', function () { + it('should enable a filter that is disabled', function () { + const disabledFilter = { + meta: { + disabled: true, + }, + match_all: {}, + }; + + expect(enableFilter(disabledFilter).meta).to.have.property('disabled', false); + }); + + it('should explicitly enable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(enableFilter(enabledFilter).meta).to.have.property('disabled', false); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const enabledFilter = enableFilter(enabledFilterWithProperties); + expect(enabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(enabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); + +describe('function toggleFilterDisabled', function () { + it('should enable a filter that is disabled', function () { + const disabledFilter = { + meta: { + disabled: true, + }, + match_all: {}, + }; + + expect(toggleFilterDisabled(disabledFilter).meta).to.have.property('disabled', false); + }); + + it('should disable a filter that is explicitly enabled', function () { + const enabledFilter = { + meta: { + disabled: false, + }, + match_all: {}, + }; + + expect(toggleFilterDisabled(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should disable a filter that is implicitly enabled', function () { + const enabledFilter = { + match_all: {}, + }; + + expect(toggleFilterDisabled(enabledFilter).meta).to.have.property('disabled', true); + }); + + it('should preserve other properties', function () { + const enabledFilterWithProperties = { + meta: { + meta_property: 'META_PROPERTY', + }, + match_all: {}, + }; + + const disabledFilter = toggleFilterDisabled(enabledFilterWithProperties); + expect(disabledFilter).to.have.property('match_all', enabledFilterWithProperties.match_all); + expect(disabledFilter.meta).to.have.property('meta_property', enabledFilterWithProperties.meta_property); + }); +}); diff --git a/src/ui/public/filter_bar/lib/disable_filter.js b/src/ui/public/filter_bar/lib/disable_filter.js new file mode 100644 index 000000000000000..099af1b9b19b3e6 --- /dev/null +++ b/src/ui/public/filter_bar/lib/disable_filter.js @@ -0,0 +1,25 @@ +export function disableFilter(filter) { + return setFilterDisabled(filter, true); +} + +export function enableFilter(filter) { + return setFilterDisabled(filter, false); +} + +export function toggleFilterDisabled(filter) { + const { meta: { disabled = false } = {} } = filter; + + return setFilterDisabled(filter, !disabled); +} + +function setFilterDisabled(filter, disabled) { + const { meta = {} } = filter; + + return { + ...filter, + meta: { + ...meta, + disabled, + } + }; +} diff --git a/test/functional/apps/context/_discover_navigation.js b/test/functional/apps/context/_discover_navigation.js index 87bf821bef66995..8eecaabca7eb983 100644 --- a/test/functional/apps/context/_discover_navigation.js +++ b/test/functional/apps/context/_discover_navigation.js @@ -3,10 +3,12 @@ import expect from 'expect.js'; const TEST_DISCOVER_START_TIME = '2015-09-19 06:31:44.000'; const TEST_DISCOVER_END_TIME = '2015-09-23 18:31:44.000'; const TEST_COLUMN_NAMES = ['@message']; +const TEST_FILTER_COLUMN_NAMES = [['extension', 'jpg'], ['geo.src', 'IN']]; export default function ({ getService, getPageObjects }) { const retry = getService('retry'); const docTable = getService('docTable'); + const filterBar = getService('filterBar'); const PageObjects = getPageObjects(['common', 'header', 'discover']); describe('context link in discover', function contextSize() { @@ -16,20 +18,26 @@ export default function ({ getService, getPageObjects }) { await Promise.all(TEST_COLUMN_NAMES.map((columnName) => ( PageObjects.discover.clickFieldListItemAdd(columnName) ))); + await Promise.all(TEST_FILTER_COLUMN_NAMES.map(async ([columnName, value]) => { + await PageObjects.discover.clickFieldListItem(columnName); + await PageObjects.discover.clickFieldListPlusFilter(columnName, value); + })); }); it('should open the context view with the selected document as anchor', async function () { const discoverDocTable = await docTable.getTable(); const firstRow = (await docTable.getBodyRows(discoverDocTable))[0]; + + // get the timestamp of the first row const firstTimestamp = await (await docTable.getFields(firstRow))[0] .getVisibleText(); - // add a column in Discover + // navigate to the context view await (await docTable.getRowExpandToggle(firstRow)).click(); const firstDetailsRow = (await docTable.getDetailsRows(discoverDocTable))[0]; await (await docTable.getRowActions(firstDetailsRow))[0].click(); - // check the column in the Context View + // check the anchor timestamp in the context view await retry.try(async () => { const contextDocTable = await docTable.getTable(); const anchorRow = await docTable.getAnchorRow(contextDocTable); @@ -52,6 +60,16 @@ export default function ({ getService, getPageObjects }) { ]); }); }); + + it('should open the context view with the filters disabled', async function () { + const hasDisabledFilters = ( + await Promise.all(TEST_FILTER_COLUMN_NAMES.map( + ([columnName, value]) => filterBar.hasFilter(columnName, value, false) + )) + ).reduce((result, hasDisabledFilter) => result && hasDisabledFilter, true); + + expect(hasDisabledFilters).to.be(true); + }); }); } diff --git a/test/functional/apps/context/_filters.js b/test/functional/apps/context/_filters.js new file mode 100644 index 000000000000000..4384023df3c4601 --- /dev/null +++ b/test/functional/apps/context/_filters.js @@ -0,0 +1,62 @@ +import expect from 'expect.js'; + +const TEST_ANCHOR_ID = 'AU_x3_BrGFA8no6QjjaI'; +const TEST_ANCHOR_TYPE = 'apache'; +const TEST_ANCHOR_FILTER_FIELD = 'geo.src'; +const TEST_ANCHOR_FILTER_VALUE = 'IN'; +const TEST_COLUMN_NAMES = ['extension', 'geo.src']; +const TEST_INDEX_PATTERN = 'logstash-*'; + +export default function ({ getService, getPageObjects }) { + const docTable = getService('docTable'); + const filterBar = getService('filterBar'); + const PageObjects = getPageObjects(['common', 'context']); + + describe('context filters', function contextSize() { + before(async function() { + await PageObjects.context.navigateTo(TEST_INDEX_PATTERN, TEST_ANCHOR_TYPE, TEST_ANCHOR_ID, { + columns: TEST_COLUMN_NAMES, + }); + }); + + it('should be addable via expanded doc table rows', async function () { + const table = await docTable.getTable(); + const anchorRow = await docTable.getAnchorRow(table); + + await docTable.toggleRowExpanded(anchorRow); + + const anchorDetailsRow = await docTable.getAnchorDetailsRow(table); + await docTable.addInclusiveFilter(anchorDetailsRow, TEST_ANCHOR_FILTER_FIELD); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + + await docTable.toggleRowExpanded(anchorRow); + + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, true)).to.be(true); + + const rows = await docTable.getBodyRows(table); + const hasOnlyFilteredRows = ( + await Promise.all(rows.map( + async (row) => await (await docTable.getFields(row))[2].getVisibleText() + )) + ).every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); + expect(hasOnlyFilteredRows).to.be(true); + }); + + it('should be toggleable via the filter bar', async function () { + const table = await docTable.getTable(); + + await filterBar.toggleFilterEnabled(TEST_ANCHOR_FILTER_FIELD); + await PageObjects.context.waitUntilContextLoadingHasFinished(); + + expect(await filterBar.hasFilter(TEST_ANCHOR_FILTER_FIELD, TEST_ANCHOR_FILTER_VALUE, false)).to.be(true); + + const rows = await docTable.getBodyRows(table); + const hasOnlyFilteredRows = ( + await Promise.all(rows.map( + async (row) => await (await docTable.getFields(row))[2].getVisibleText() + )) + ).every((fieldContent) => fieldContent === TEST_ANCHOR_FILTER_VALUE); + expect(hasOnlyFilteredRows).to.be(false); + }); + }); +} diff --git a/test/functional/apps/context/index.js b/test/functional/apps/context/index.js index 9868272186510d1..1fb187ab0ed8235 100644 --- a/test/functional/apps/context/index.js +++ b/test/functional/apps/context/index.js @@ -22,6 +22,7 @@ export default function ({ getService, getPageObjects, loadTestFile }) { }); loadTestFile(require.resolve('./_discover_navigation')); + loadTestFile(require.resolve('./_filters')); loadTestFile(require.resolve('./_size')); }); diff --git a/test/functional/config.js b/test/functional/config.js index a4a1d75d81eec8e..6d6cc50dc720f89 100644 --- a/test/functional/config.js +++ b/test/functional/config.js @@ -15,6 +15,7 @@ import { import { RemoteProvider, + FilterBarProvider, FindProvider, RetryProvider, TestSubjectsProvider, @@ -55,6 +56,7 @@ export default async function ({ readConfigFile }) { esArchiver: commonConfig.get('services.esArchiver'), kibanaServer: commonConfig.get('services.kibanaServer'), remote: RemoteProvider, + filterBar: FilterBarProvider, find: FindProvider, retry: RetryProvider, testSubjects: TestSubjectsProvider, diff --git a/test/functional/services/doc_table.js b/test/functional/services/doc_table.js index 4463556a38e8bd7..98a29b22c7676e3 100644 --- a/test/functional/services/doc_table.js +++ b/test/functional/services/doc_table.js @@ -14,6 +14,10 @@ export function DocTableProvider({ getService }) { return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"]'); } + async getAnchorDetailsRow(table) { + return await table.findByCssSelector('[data-test-subj~="docTableAnchorRow"] + tr'); + } + async getRowExpandToggle(row) { return await row.findByCssSelector('[data-test-subj~="docTableExpandToggleColumn"]'); } @@ -33,6 +37,25 @@ export function DocTableProvider({ getService }) { async getHeaderFields(table) { return await table.findAllByCssSelector('[data-test-subj~="docTableHeaderField"]'); } + + async getTableDocViewRow(detailsRow, fieldName) { + return await detailsRow.findByCssSelector(`[data-test-subj~="tableDocViewRow-${fieldName}"]`); + } + + async getAddInclusiveFilterButton(tableDocViewRow) { + return await tableDocViewRow.findByCssSelector(`[data-test-subj~="addInclusiveFilterButton"]`); + } + + async addInclusiveFilter(detailsRow, fieldName) { + const tableDocViewRow = await this.getTableDocViewRow(detailsRow, fieldName); + const addInclusiveFilterButton = await this.getAddInclusiveFilterButton(tableDocViewRow); + await addInclusiveFilterButton.click(); + } + + async toggleRowExpanded(row) { + const rowExpandToggle = await this.getRowExpandToggle(row); + return await rowExpandToggle.click(); + } } return new DocTable(); diff --git a/test/functional/services/filter_bar.js b/test/functional/services/filter_bar.js new file mode 100644 index 000000000000000..b62f2db00cff126 --- /dev/null +++ b/test/functional/services/filter_bar.js @@ -0,0 +1,21 @@ +export function FilterBarProvider({ getService }) { + const remote = getService('remote'); + const testSubjects = getService('testSubjects'); + + class FilterBar { + hasFilter(key, value, enabled = true) { + const filterActivationState = enabled ? 'enabled' : 'disabled'; + return testSubjects.exists( + `filter & filter-key-${key} & filter-value-${value} & filter-${filterActivationState}` + ); + } + + async toggleFilterEnabled(key) { + const filterElement = await testSubjects.find(`filter & filter-key-${key}`); + await remote.moveMouseTo(filterElement); + await testSubjects.find(`filter & filter-key-${key} disableFilter-${key}`).click(); + } + } + + return new FilterBar(); +} diff --git a/test/functional/services/index.js b/test/functional/services/index.js index 33c2fc4765146d2..0ec496ceadbc37a 100644 --- a/test/functional/services/index.js +++ b/test/functional/services/index.js @@ -1,4 +1,5 @@ export { RetryProvider } from './retry'; +export { FilterBarProvider } from './filter_bar'; export { FindProvider } from './find'; export { TestSubjectsProvider } from './test_subjects'; export { RemoteProvider } from './remote';