[context view] Apply filters to the context query (#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.
This commit is contained in:
Felix Stürmer 2017-05-18 12:05:38 +02:00 committed by GitHub
parent 8a765a3769
commit bc64b9a3aa
33 changed files with 560 additions and 98 deletions

View file

@ -1,6 +1,6 @@
<table class="table table-condensed">
<tbody>
<tr ng-repeat="field in fields">
<tr ng-repeat="field in fields" data-test-subj="tableDocViewRow-{{ field }}">
<td field-name="field"
field-type="mapping[field].type"
width="1%"
@ -11,6 +11,7 @@
<button
class="doc-viewer-button"
ng-click="filter(mapping[field], flattened[field], '+')"
data-test-subj="addInclusiveFilterButton"
>
<i
tooltip="Filter for value"

View file

@ -8,36 +8,38 @@ import { fetchAnchorProvider } from '../anchor';
describe('context app', function () {
let fetchAnchor;
let SearchSourceStub;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function createStubs(Private) {
SearchSourceStub = createSearchSourceStubProvider([
{ _id: 'hit1' },
]);
Private.stub(SearchSourceProvider, SearchSourceStub);
fetchAnchor = Private(fetchAnchorProvider);
}));
describe('function fetchAnchor', function () {
let fetchAnchor;
let SearchSourceStub;
beforeEach(ngMock.module(function createServiceStubs($provide) {
$provide.value('courier', createCourierStub());
}));
beforeEach(ngMock.inject(function createPrivateStubs(Private) {
SearchSourceStub = createSearchSourceStubProvider([
{ _id: 'hit1' },
]);
Private.stub(SearchSourceProvider, SearchSourceStub);
fetchAnchor = Private(fetchAnchorProvider);
}));
it('should use the `fetch` method of the SearchSource', function () {
const indexPatternStub = createIndexPatternStub('index1');
const searchSourceStub = new SearchSourceStub();
return fetchAnchor(indexPatternStub, 'UID', { '@timestamp': 'desc' })
return fetchAnchor('INDEX_PATTERN_ID', 'UID', { '@timestamp': 'desc' })
.then(() => {
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({

View file

@ -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)

View file

@ -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: {},
})

View file

@ -2,7 +2,7 @@
<div class="kuiLocalNavRow">
<div class="kuiLocalNavRow__section">
<div class="kuiLocalTitle">
Surrounding Documents in {{ contextApp.state.queryParameters.indexPattern.id }}
Surrounding Documents in {{ contextApp.state.queryParameters.indexPatternId }}
</div>
</div>
</div>
@ -13,6 +13,8 @@
</div>
</div>
<filter-bar></filter-bar>
<!-- Error feedback -->
<div
class="kuiViewContent kuiViewContentItem"
@ -96,8 +98,9 @@
>
<div class="discover-table" fixed-scroll>
<doc-table
filter="contextApp.actions.addFilter"
hits="contextApp.state.rows.all"
index-pattern="contextApp.state.queryParameters.indexPattern"
index-pattern="contextApp.indexPattern"
sorting="contextApp.state.queryParameters.sort"
columns="contextApp.state.queryParameters.columns"
infinite-scroll="true"

View file

@ -33,6 +33,7 @@ module.directive('contextApp', function ContextApp() {
anchorUid: '=',
columns: '=',
indexPattern: '=',
filters: '=',
predecessorCount: '=',
successorCount: '=',
sort: '=',
@ -71,22 +72,41 @@ function ContextAppController($scope, config, Private, timefilter) {
], (newValues) => this.actions.setAllRows(...newValues));
/**
* Sync query parameters to arguments
* Sync properties to state
*/
$scope.$watchCollection(
() => _.pick(this, QUERY_PARAMETER_KEYS),
(newValues) => {
// break the watch cycle
if (!_.isEqual(newValues, this.state.queryParameters)) {
this.actions.fetchAllRowsWithNewQueryParameters(newValues);
() => ({
...(_.pick(this, QUERY_PARAMETER_KEYS)),
indexPatternId: this.indexPattern.id,
}),
(newQueryParameters) => {
const { queryParameters } = this.state;
if (
(newQueryParameters.indexPatternId !== queryParameters.indexPatternId)
|| (newQueryParameters.anchorUid !== queryParameters.anchorUid)
|| (!_.isEqual(newQueryParameters.sort, queryParameters.sort))
) {
this.actions.fetchAllRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters));
} else if (
(newQueryParameters.predecessorCount !== queryParameters.predecessorCount)
|| (newQueryParameters.successorCount !== queryParameters.successorCount)
|| (!_.isEqual(newQueryParameters.filters, queryParameters.filters))
) {
this.actions.fetchContextRowsWithNewQueryParameters(_.cloneDeep(newQueryParameters));
}
},
);
/**
* Sync state to properties
*/
$scope.$watchCollection(
() => this.state.queryParameters,
(newValues) => {
_.assign(this, newValues);
() => ({
predecessorCount: this.state.queryParameters.predecessorCount,
successorCount: this.state.queryParameters.successorCount,
}),
(newParameters) => {
_.assign(this, newParameters);
},
);
}

View file

@ -3,7 +3,8 @@
columns="contextAppRoute.state.columns"
discover-url="contextAppRoute.discoverUrl"
index-pattern="contextAppRoute.indexPattern"
filters="contextAppRoute.filters"
predecessor-count="contextAppRoute.state.predecessorCount"
successor-count="contextAppRoute.state.successorCount"
sort="[contextAppRoute.indexPattern.timeFieldName, 'desc']"
sort="contextAppRoute.state.sort"
></context-app>

View file

@ -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),
};
}

View file

@ -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,

View file

@ -5,6 +5,7 @@ export function createStateStub(overrides) {
return _.merge({
queryParameters: {
defaultStepSize: 3,
indexPatternId: 'INDEX_PATTERN_ID',
predecessorCount: 10,
successorCount: 10,
},

View file

@ -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(),
};
}

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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)({

View file

@ -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);

View file

@ -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,

View file

@ -3,7 +3,8 @@ export function createInitialQueryParametersState(defaultStepSize) {
anchorUid: null,
columns: [],
defaultStepSize,
indexPattern: null,
filters: [],
indexPatternId: null,
predecessorCount: 0,
successorCount: 0,
sort: [],

View file

@ -144,6 +144,7 @@
columns="state.columns"
infinite-scroll="true"
filter="filterQuery"
filters="state.filters"
data-shared-item
data-title="{{opts.savedSearch.lastSavedTitle}}"
data-description="{{opts.savedSearch.description}}"

View file

@ -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}`;

View file

@ -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' : ''}}"

View file

@ -23,6 +23,7 @@ uiModules.get('kibana')
searchSource: '=?',
infiniteScroll: '=?',
filter: '=?',
filters: '=?',
onAddColumn: '=?',
onChangeSortOrder: '=?',
onMoveColumn: '=?',

View file

@ -23,7 +23,12 @@
</div>
<div class="filter-bar" ng-show="filters.length">
<div class="filter" ng-class="{ negate: filter.meta.negate, disabled: filter.meta.disabled }" ng-repeat="filter in filters track by $index">
<div
class="filter"
ng-class="{ negate: filter.meta.negate, disabled: filter.meta.disabled }"
ng-repeat="filter in filters track by $index"
data-test-subj="filter filter-{{ filter.meta.disabled ? 'disabled' : 'enabled' }} {{ filter.meta.key ? 'filter-key-' + filter.meta.key : '' }} {{ filter.meta.value ? 'filter-value-' + filter.meta.value : '' }}"
>
<div class="filter-description">
<span ng-if="filter.$state.store == 'globalState'"><i class="fa fa-fw fa-thumb-tack pinned"></i></span>
<span ng-if="filter.meta.alias">{{ filter.meta.alias }}</span>

View file

@ -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);

View file

@ -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);
});
});

View file

@ -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,
}
};
}

View file

@ -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);
});
});
}

View file

@ -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);
});
});
}

View file

@ -16,6 +16,7 @@ export default function ({ getService, getPageObjects, loadTestFile }) {
});
loadTestFile(require.resolve('./_discover_navigation'));
loadTestFile(require.resolve('./_filters'));
loadTestFile(require.resolve('./_size'));
});

View file

@ -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,

View file

@ -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();

View file

@ -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();
}

View file

@ -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';