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