diff --git a/src/core_plugins/table_vis/public/legacy_response_handler.js b/src/core_plugins/table_vis/public/legacy_response_handler.js
index b8f12f00c4bb6..74ca19df1d1fd 100644
--- a/src/core_plugins/table_vis/public/legacy_response_handler.js
+++ b/src/core_plugins/table_vis/public/legacy_response_handler.js
@@ -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;
});
})
}];
diff --git a/src/ui/public/directives/rows.js b/src/ui/public/directives/rows.js
index 97e7a890ce51f..0eff91abe6a7c 100644
--- a/src/ui/public/directives/rows.js
+++ b/src/ui/public/directives/rows.js
@@ -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);
diff --git a/src/ui/public/vis/response_handlers/legacy.js b/src/ui/public/vis/response_handlers/legacy.js
index e9f06aceaa4b4..2bcb2f1f1052f 100644
--- a/src/ui/public/vis/response_handlers/legacy.js
+++ b/src/ui/public/vis/response_handlers/legacy.js
@@ -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;
diff --git a/src/ui/public/vis/vis.js b/src/ui/public/vis/vis.js
index bdb6ff4d3106f..16b92fce74533 100644
--- a/src/ui/public/vis/vis.js
+++ b/src/ui/public/vis/vis.js
@@ -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());
}
},
diff --git a/src/ui/public/vis/vis_filters.js b/src/ui/public/vis/vis_filters.js
new file mode 100644
index 0000000000000..fe24dacb61560
--- /dev/null
+++ b/src/ui/public/vis/vis_filters.js
@@ -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
+ };
+}
diff --git a/src/ui/public/vis/vis_types/vislib_vis_legend.html b/src/ui/public/vis/vis_types/vislib_vis_legend.html
index 05a39f19974f9..4faa4836145fc 100644
--- a/src/ui/public/vis/vis_types/vislib_vis_legend.html
+++ b/src/ui/public/vis/vis_types/vislib_vis_legend.html
@@ -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"
>
@@ -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"
>
@@ -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"
>
diff --git a/src/ui/public/vis/vis_types/vislib_vis_legend.js b/src/ui/public/vis/vis_types/vislib_vis_legend.js
index 4787b54e115de..ccb6b3da69884 100644
--- a/src/ui/public/vis/vis_types/vislib_vis_legend.js
+++ b/src/ui/public/vis/vis_types/vislib_vis_legend.js
@@ -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;
};
diff --git a/test/functional/apps/visualize/_data_table.js b/test/functional/apps/visualize/_data_table.js
index 10664b5c35266..5cd9b280c5550 100644
--- a/test/functional/apps/visualize/_data_table.js
+++ b/test/functional/apps/visualize/_data_table.js
@@ -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();
diff --git a/test/functional/apps/visualize/_pie_chart.js b/test/functional/apps/visualize/_pie_chart.js
index 6427df4dc38b1..ffdde2fbc6e6a 100644
--- a/test/functional/apps/visualize/_pie_chart.js
+++ b/test/functional/apps/visualize/_pie_chart.js
@@ -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',
diff --git a/test/functional/page_objects/visualize_page.js b/test/functional/page_objects/visualize_page.js
index 9e742f1295933..268932fd08bdf 100644
--- a/test/functional/page_objects/visualize_page.js
+++ b/test/functional/page_objects/visualize_page.js
@@ -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}`);
}