other bucket filter for table and vislib legend (#24473) (#25220)

This commit is contained in:
Peter Pisljar 2018-11-06 18:55:48 +01:00 committed by GitHub
parent 0879295771
commit ff4e773c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 186 additions and 70 deletions

View file

@ -52,9 +52,15 @@ export function splitTable(columns, rows, $parent) {
return [{
$parent,
columns: columns.map(column => ({ title: column.name, ...column })),
rows: rows.map(row => {
rows: rows.map((row, rowIndex) => {
return columns.map(column => {
return new AggConfigResult(column.aggConfig, $parent, row[column.id], row[column.id]);
const aggConfigResult = new AggConfigResult(column.aggConfig, $parent, row[column.id], row[column.id]);
aggConfigResult.rawData = {
table: { columns, rows },
column: columns.findIndex(c => c.id === column.id),
row: rowIndex,
};
return aggConfigResult;
});
})
}];

View file

@ -20,18 +20,18 @@
import $ from 'jquery';
import _ from 'lodash';
import AggConfigResult from '../vis/agg_config_result';
import { FilterBarClickHandlerProvider } from '../filter_bar/filter_bar_click_handler';
import { uiModules } from '../modules';
import tableCellFilterHtml from './partials/table_cell_filter.html';
import { isNumeric } from '../utils/numeric';
import { VisFiltersProvider } from '../vis/vis_filters';
const module = uiModules.get('kibana');
module.directive('kbnRows', function ($compile, $rootScope, getAppState, Private) {
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
module.directive('kbnRows', function ($compile, Private) {
return {
restrict: 'A',
link: function ($scope, $el, attr) {
const visFilter = Private(VisFiltersProvider);
function addCell($tr, contents) {
function createCell() {
return $(document.createElement('td'));
@ -43,15 +43,13 @@ module.directive('kbnRows', function ($compile, $rootScope, getAppState, Private
const scope = $scope.$new();
const $state = getAppState();
const addFilter = filterBarClickHandler($state);
scope.onFilterClick = (event, negate) => {
// Don't add filter if a link was clicked.
if ($(event.target).is('a')) {
return;
}
addFilter({ point: { aggConfigResult: aggConfigResult }, negate });
visFilter.filter({ datum: { aggConfigResult: aggConfigResult }, negate });
};
return $compile($template)(scope);

View file

@ -76,8 +76,8 @@ const LegacyResponseHandlerProvider = function () {
const aggConfigResult = new AggConfigResult(column.aggConfig, previousSplitAgg, value, value);
aggConfigResult.rawData = {
table: table,
columnIndex: table.columns.findIndex(c => c.id === column.id),
rowIndex: rowIndex,
column: table.columns.findIndex(c => c.id === column.id),
row: rowIndex,
};
if (column.aggConfig.type.type === 'buckets') {
previousSplitAgg = aggConfigResult;

View file

@ -34,38 +34,18 @@ import { AggConfigs } from './agg_configs';
import { PersistedState } from '../persisted_state';
import { onBrushEvent } from '../utils/brush_event';
import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter';
import { FilterBarPushFiltersProvider } from '../filter_bar/push_filters';
import { updateVisualizationConfig } from './vis_update';
import { SearchSourceProvider } from '../courier/search_source';
import { SavedObjectsClientProvider } from '../saved_objects';
import { timefilter } from 'ui/timefilter';
const getTerms = (table, columnIndex, rowIndex) => {
if (rowIndex === -1) {
return [];
}
// get only rows where cell value matches current row for all the fields before columnIndex
const rows = table.rows.filter(row => {
return table.columns.every((column, i) => {
return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex;
});
});
const terms = rows.map(row => row[table.columns[columnIndex].id]);
return [...new Set(terms.filter(term => {
const notOther = term !== '__other__';
const notMissing = term !== '__missing__';
return notOther && notMissing;
}))];
};
import { VisFiltersProvider } from './vis_filters';
export function VisProvider(Private, indexPatterns, getAppState) {
const visTypes = Private(VisTypesRegistryProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
const SearchSource = Private(SearchSourceProvider);
const savedObjectsClient = Private(SavedObjectsClientProvider);
const filterBarPushFilters = Private(FilterBarPushFiltersProvider);
const visFilter = Private(VisFiltersProvider);
class Vis extends EventEmitter {
constructor(indexPattern, visState) {
@ -95,37 +75,10 @@ export function VisProvider(Private, indexPatterns, getAppState) {
events: {
// the filter method will be removed in the near feature
// you should rather use addFilter method below
filter: (event) => {
let data = event.datum.aggConfigResult;
const filters = [];
while (data.$parent) {
const { key, rawData } = data.$parent;
const { table, column, row } = rawData;
filters.push(this.API.events.createFilter(table, column, row, key));
data = data.$parent;
}
const appState = getAppState();
filterBarPushFilters(appState)(_.flatten(filters));
},
createFilter: (data, columnIndex, rowIndex, cellValue) => {
const { aggConfig, id: columnId } = data.columns[columnIndex];
let filter = [];
const value = rowIndex > -1 ? data.rows[rowIndex][columnId] : cellValue;
if (value === null || value === undefined) {
return;
}
if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) {
const terms = getTerms(data, columnIndex, rowIndex);
filter = aggConfig.createFilter(value, { terms });
} else {
filter = aggConfig.createFilter(value);
}
return filter;
},
addFilter: (data, columnIndex, rowIndex, cellValue) => {
const filter = this.API.events.createFilter(data, columnIndex, rowIndex, cellValue);
queryFilter.addFilters(filter);
}, brush: (event) => {
filter: visFilter.filter,
createFilter: visFilter.createFilter,
addFilter: visFilter.addFilter,
brush: (event) => {
onBrushEvent(event, getAppState());
}
},

View file

@ -0,0 +1,100 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
import { FilterBarPushFiltersProvider } from '../filter_bar/push_filters';
import { FilterBarQueryFilterProvider } from '../filter_bar/query_filter';
const getTerms = (table, columnIndex, rowIndex) => {
if (rowIndex === -1) {
return [];
}
// get only rows where cell value matches current row for all the fields before columnIndex
const rows = table.rows.filter(row => {
return table.columns.every((column, i) => {
return row[column.id] === table.rows[rowIndex][column.id] || i >= columnIndex;
});
});
const terms = rows.map(row => row[table.columns[columnIndex].id]);
return [...new Set(terms.filter(term => {
const notOther = term !== '__other__';
const notMissing = term !== '__missing__';
return notOther && notMissing;
}))];
};
export function VisFiltersProvider(Private, getAppState) {
const filterBarPushFilters = Private(FilterBarPushFiltersProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
const createFilter = (data, columnIndex, rowIndex, cellValue) => {
const { aggConfig, id: columnId } = data.columns[columnIndex];
let filter = [];
const value = rowIndex > -1 ? data.rows[rowIndex][columnId] : cellValue;
if (value === null || value === undefined) {
return;
}
if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) {
const terms = getTerms(data, columnIndex, rowIndex);
filter = aggConfig.createFilter(value, { terms });
} else {
filter = aggConfig.createFilter(value);
}
return filter;
};
const filter = (event, { simulate } = {}) => {
let data = event.datum.aggConfigResult;
const filters = [];
while (data) {
if (data.type === 'bucket') {
const { key, rawData } = data;
const { table, column, row } = rawData;
const filter = createFilter(table, column, row, key);
if (event.negate) {
if (Array.isArray(filter)) {
filter.forEach(f => f.meta.negate = !f.meta.negate);
} else {
filter.meta.negate = !filter.meta.negate;
}
}
filters.push(filter);
}
data = data.$parent;
}
if (!simulate) {
const appState = getAppState();
filterBarPushFilters(appState)(_.flatten(filters));
}
return filters;
};
const addFilter = (data, columnIndex, rowIndex, cellValue) => {
const filter = createFilter(data, columnIndex, rowIndex, cellValue);
queryFilter.addFilters(filter);
};
return {
createFilter,
addFilter,
filter
};
}

View file

@ -6,6 +6,7 @@
aria-label="{{::'common.ui.vis.visTypes.legend.toggleLegendButtonAriaLabel' | i18n: { defaultMessage: 'Toggle legend' } }}"
aria-expanded="{{!!open}}"
aria-controls="{{::legendId}}"
data-test-subj="vislibToggleLegend"
>
<span class="kuiIcon {{getToggleLegendClasses()}}"></span>
</button>
@ -50,6 +51,7 @@
class="kuiButton kuiButton--basic kuiButton--small"
ng-click="filter(legendData, false)"
aria-label="{{::'common.ui.vis.visTypes.legend.filterForValueButtonAriaLabel' | i18n: { defaultMessage: 'Filter for value {legendDataLabel}', values: { legendDataLabel: legendData.label } } }}"
data-test-subj="legend-{{legendData.label}}-filterIn"
>
<span class="kuiIcon fa-search-plus"></span>
</button>
@ -58,6 +60,7 @@
class="kuiButton kuiButton--basic kuiButton--small"
ng-click="filter(legendData, true)"
aria-label="{{::'common.ui.vis.visTypes.legend.filterOutValueButtonAriaLabel' | i18n: { defaultMessage: 'Filter out value {legendDataLabel}', values: { legendDataLabel: legendData.label } } }}"
data-test-subj="legend-{{legendData.label}}-filterOut"
>
<span class="kuiIcon fa-search-minus"></span>
</button>

View file

@ -20,22 +20,18 @@
import _ from 'lodash';
import html from './vislib_vis_legend.html';
import { VislibLibDataProvider } from '../../vislib/lib/data';
import { FilterBarClickHandlerProvider } from '../../filter_bar/filter_bar_click_handler';
import { uiModules } from '../../modules';
import { htmlIdGenerator, keyCodes } from '@elastic/eui';
uiModules.get('kibana')
.directive('vislibLegend', function (Private, getAppState, $timeout, i18n) {
.directive('vislibLegend', function (Private, $timeout, i18n) {
const Data = Private(VislibLibDataProvider);
const filterBarClickHandler = Private(FilterBarClickHandlerProvider);
return {
restrict: 'E',
template: html,
link: function ($scope) {
const $state = getAppState();
const clickHandler = filterBarClickHandler($state);
$scope.legendId = htmlIdGenerator()('legend');
$scope.open = $scope.uiState.get('vis.legendOpen', true);
@ -104,11 +100,11 @@ uiModules.get('kibana')
};
$scope.filter = function (legendData, negate) {
clickHandler({ point: legendData, negate: negate });
$scope.vis.API.events.filter({ datum: legendData.values, negate: negate });
};
$scope.canFilter = function (legendData) {
const filters = clickHandler({ point: legendData }, true) || [];
const filters = $scope.vis.API.events.filter({ datum: legendData.values }, { simulate: true });
return filters.length;
};

View file

@ -193,6 +193,42 @@ export default function ({ getService, getPageObjects }) {
expect(data.length).to.be.greaterThan(0);
});
describe('otherBucket', () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();
await PageObjects.visualize.clickDataTable();
await PageObjects.visualize.clickNewSearch();
await PageObjects.header.setAbsoluteRange(fromTime, toTime);
await PageObjects.visualize.clickBucket('Split Rows');
await PageObjects.visualize.selectAggregation('Terms');
await PageObjects.visualize.selectField('extension.raw');
await PageObjects.visualize.setSize(2);
await PageObjects.visualize.toggleOtherBucket();
await PageObjects.visualize.toggleMissingBucket();
await PageObjects.visualize.clickGo();
});
it('should show correct data', async () => {
const data = await PageObjects.visualize.getTableVisContent();
expect(data).to.be.eql([
[ 'jpg', '9,109' ],
[ 'css', '2,159' ],
[ 'Other', '2,736' ]
]);
});
it('should apply correct filter', async () => {
await PageObjects.visualize.filterOnTableCell(1, 3);
await PageObjects.header.waitUntilLoadingHasFinished();
const data = await PageObjects.visualize.getTableVisContent();
expect(data).to.be.eql([
[ 'png', '1,373' ],
[ 'gif', '918' ],
[ 'Other', '445' ]
]);
});
});
describe('metricsOnAllLevels', () => {
before(async () => {
await PageObjects.visualize.navigateToNewVisualization();

View file

@ -126,6 +126,17 @@ export default function ({ getService, getPageObjects }) {
await filterBar.removeFilter('machine.os.raw');
});
it('should apply correct filter on other bucket by clicking on a legend', async () => {
const expectedTableData = [ 'Missing', 'osx' ];
await PageObjects.visualize.filterLegend('Other');
await PageObjects.header.waitUntilLoadingHasFinished();
const pieData = await PageObjects.visualize.getPieChartLabels();
log.debug(`pieData.length = ${pieData.length}`);
expect(pieData).to.eql(expectedTableData);
await filterBar.removeFilter('machine.os.raw');
});
it('should show two levels of other buckets', async () => {
const expectedTableData = [ 'win 8', 'CN', 'IN', 'US', 'ID', 'BR', 'Other', 'win xp',
'CN', 'IN', 'US', 'ID', 'BR', 'Other', 'win 7', 'CN', 'IN', 'US', 'ID', 'BR', 'Other',

View file

@ -1154,6 +1154,19 @@ export function VisualizePageProvider({ getService, getPageObjects }) {
await filterBtn.click();
}
async toggleLegend(show = true) {
const isVisible = remote.findByCssSelector('vislib-legend .legend-ul');
if ((show && !isVisible) || (!show && isVisible)) {
await testSubjects.click('vislibToggleLegend');
}
}
async filterLegend(name) {
await this.toggleLegend();
await testSubjects.click(`legend-${name}`);
await testSubjects.click(`legend-${name}-filterIn`);
}
async doesLegendColorChoiceExist(color) {
return await testSubjects.exists(`legendSelectColor-${color}`);
}