From 389bcdcf442df57212b1702b33923b07983b65ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Thu, 18 May 2017 22:38:06 +0200 Subject: [PATCH] [5.x] [context view] Apply filters to the context query (#11890) Backports PR #11466 This adds the ability to display a filter bar in the Context view and to apply those filters to the queries. It also modifies the link from the Discover view to the Context view to copy the currently defined filters when switching. New filters can be added from within the Context view using the icons in the expanded detail rows. --- .../kbn_doc_views/public/views/table.html | 3 +- .../public/context/api/__tests__/anchor.js | 67 +++++----- .../kibana/public/context/api/anchor.js | 6 +- .../kibana/public/context/api/context.js | 22 ++-- .../kibana/public/context/app.html | 7 +- src/core_plugins/kibana/public/context/app.js | 38 ++++-- .../kibana/public/context/index.html | 3 +- .../kibana/public/context/index.js | 22 +++- .../kibana/public/context/query/actions.js | 33 +++-- .../query_parameters/__tests__/_utils.js | 1 + .../__tests__/action_add_filter.js | 53 ++++++++ .../action_increase_predecessor_count.js | 17 ++- .../action_increase_successor_count.js | 17 ++- .../__tests__/action_set_predecessor_count.js | 16 ++- .../__tests__/action_set_query_parameters.js | 21 ++- .../__tests__/action_set_successor_count.js | 16 ++- .../context/query_parameters/actions.js | 13 +- .../public/context/query_parameters/state.js | 3 +- .../kibana/public/discover/index.html | 1 + .../public/doc_table/components/table_row.js | 4 + src/ui/public/doc_table/doc_table.html | 2 + src/ui/public/doc_table/doc_table.js | 1 + src/ui/public/filter_bar/filter_bar.html | 7 +- src/ui/public/filter_bar/filter_bar.js | 5 +- .../lib/__tests__/disable_filter.js | 121 ++++++++++++++++++ .../public/filter_bar/lib/disable_filter.js | 25 ++++ .../apps/context/_discover_navigation.js | 22 +++- test/functional/apps/context/_filters.js | 62 +++++++++ test/functional/apps/context/index.js | 1 + test/functional/config.js | 2 + test/functional/services/doc_table.js | 23 ++++ test/functional/services/filter_bar.js | 21 +++ test/functional/services/index.js | 1 + 33 files changed, 559 insertions(+), 97 deletions(-) create mode 100644 src/core_plugins/kibana/public/context/query_parameters/__tests__/action_add_filter.js create mode 100644 src/ui/public/filter_bar/lib/__tests__/disable_filter.js create mode 100644 src/ui/public/filter_bar/lib/disable_filter.js create mode 100644 test/functional/apps/context/_filters.js create mode 100644 test/functional/services/filter_bar.js 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 d98f5be7512a5..7de7695318d35 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 4c395473cba4b..66c6784674753 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 808457566291b..bd5d678d768dd 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 5a0b193ebc972..fef98142f1ce2 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 07f8f750bd30d..cc9c6cb36f7d7 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 72ffc1be64a9d..e05b6c00c75c6 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 70328e9fc4fa3..73044c011df69 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 0000000000000..b0b75f613511a --- /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 0f787515f1839..6006bdaf48400 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 efa25bbe167c7..c9b7b9870d0ef 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 6d80dce42fe9c..321ea5d0ab1b0 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 7b010a512132e..f1b05278a6e97 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 4e3744708e093..be6f14795456f 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 6a838cd1374ec..b6c9c4a73b01b 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 4f8a98d8d797e..5b9023e209a30 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 80d89a7f9dc3d..fc0dc6c81804f 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 f474e35011ada..6ba4827162311 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 96b9ab144768a..bab9514e1d2ac 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 98589f5aa9069..be11e2a729b06 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 ca5ba6fa4bf1d..4598d28cddba4 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 a0de162c95a2c..b9aed3f269c6b 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 0000000000000..0d0a2faa60c99 --- /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 0000000000000..099af1b9b19b3 --- /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 87bf821bef669..8eecaabca7eb9 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 0000000000000..4384023df3c46 --- /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 9868272186510..1fb187ab0ed82 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 a4a1d75d81eec..6d6cc50dc720f 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 4463556a38e8b..98a29b22c7676 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 0000000000000..b62f2db00cff1 --- /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 33c2fc4765146..0ec496ceadbc3 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';