parent
0879295771
commit
ff4e773c52
|
@ -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;
|
||||
});
|
||||
})
|
||||
}];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
},
|
||||
|
|
100
src/ui/public/vis/vis_filters.js
Normal file
100
src/ui/public/vis/vis_filters.js
Normal 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
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue