Format the totals row (#10414)

* Format the totals row

* Add tests and format count results

* Handle the new metrics better

* Rename variable to isFieldNumeric and isFieldDate

* Add pipeline tests

* Fix timezone issues in tests
This commit is contained in:
trevan 2017-04-10 10:10:04 -06:00 committed by Peter Pisljar
parent c13e786e10
commit 74641d7d85
4 changed files with 497 additions and 13 deletions

View file

@ -110,6 +110,361 @@ data.threeTermBuckets = {
}
};
data.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative = {
hits: { total: 1000, hits: [], max_score: 0 },
aggregations: {
agg_3: {
buckets: [
{
key: 'png',
doc_count: 50,
agg_4: {
buckets: [
{
key_as_string: '2014-09-28T00:00:00.000Z',
key: 1411862400000,
doc_count: 1,
agg_1: { value: 9283 },
agg_2: { value: 1411862400000 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 23
}
}]
}
}
},
{
key_as_string: '2014-09-29T00:00:00.000Z',
key: 1411948800000,
doc_count: 2,
agg_1: { value: 28349 },
agg_2: { value: 1411948800000 },
agg_5: { value: 203 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 39
}
}]
}
}
},
{
key_as_string: '2014-09-30T00:00:00.000Z',
key: 1412035200000,
doc_count: 3,
agg_1: { value: 84330 },
agg_2: { value: 1412035200000 },
agg_5: { value: 200 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 329
}
}]
}
}
},
{
key_as_string: '2014-10-01T00:00:00.000Z',
key: 1412121600000,
doc_count: 4,
agg_1: { value: 34992 },
agg_2: { value: 1412121600000 },
agg_5: { value: 103 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 22
}
}]
}
}
},
{
key_as_string: '2014-10-02T00:00:00.000Z',
key: 1412208000000,
doc_count: 5,
agg_1: { value: 145432 },
agg_2: { value: 1412208000000 },
agg_5: { value: 153 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 93
}
}]
}
}
},
{
key_as_string: '2014-10-03T00:00:00.000Z',
key: 1412294400000,
doc_count: 35,
agg_1: { value: 220943 },
agg_2: { value: 1412294400000 },
agg_5: { value: 239 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 72
}
}]
}
}
}
]
}
},
{
key: 'css',
doc_count: 20,
agg_4: {
buckets: [
{
key_as_string: '2014-09-28T00:00:00.000Z',
key: 1411862400000,
doc_count: 1,
agg_1: { value: 9283 },
agg_2: { value: 1411862400000 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 75
}
}]
}
}
},
{
key_as_string: '2014-09-29T00:00:00.000Z',
key: 1411948800000,
doc_count: 2,
agg_1: { value: 28349 },
agg_2: { value: 1411948800000 },
agg_5: { value: 10 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 11
}
}]
}
}
},
{
key_as_string: '2014-09-30T00:00:00.000Z',
key: 1412035200000,
doc_count: 3,
agg_1: { value: 84330 },
agg_2: { value: 1412035200000 },
agg_5: { value: 24 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 238
}
}]
}
}
},
{
key_as_string: '2014-10-01T00:00:00.000Z',
key: 1412121600000,
doc_count: 4,
agg_1: { value: 34992 },
agg_2: { value: 1412121600000 },
agg_5: { value: 49 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 343
}
}]
}
}
},
{
key_as_string: '2014-10-02T00:00:00.000Z',
key: 1412208000000,
doc_count: 5,
agg_1: { value: 145432 },
agg_2: { value: 1412208000000 },
agg_5: { value: 100 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 837
}
}]
}
}
},
{
key_as_string: '2014-10-03T00:00:00.000Z',
key: 1412294400000,
doc_count: 5,
agg_1: { value: 220943 },
agg_2: { value: 1412294400000 },
agg_5: { value: 23 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 302
}
}]
}
}
}
]
}
},
{
key: 'html',
doc_count: 90,
agg_4: {
buckets: [
{
key_as_string: '2014-09-28T00:00:00.000Z',
key: 1411862400000,
doc_count: 10,
agg_1: { value: 9283 },
agg_2: { value: 1411862400000 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 30
}
}]
}
}
},
{
key_as_string: '2014-09-29T00:00:00.000Z',
key: 1411948800000,
doc_count: 20,
agg_1: { value: 28349 },
agg_2: { value: 1411948800000 },
agg_5: { value: 1 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 43
}
}]
}
}
},
{
key_as_string: '2014-09-30T00:00:00.000Z',
key: 1412035200000,
doc_count: 30,
agg_1: { value: 84330 },
agg_2: { value: 1412035200000 },
agg_5: { value: 5 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 88
}
}]
}
}
},
{
key_as_string: '2014-10-01T00:00:00.000Z',
key: 1412121600000,
doc_count: 11,
agg_1: { value: 34992 },
agg_2: { value: 1412121600000 },
agg_5: { value: 10 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 91
}
}]
}
}
},
{
key_as_string: '2014-10-02T00:00:00.000Z',
key: 1412208000000,
doc_count: 12,
agg_1: { value: 145432 },
agg_2: { value: 1412208000000 },
agg_5: { value: 43 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 534
}
}]
}
}
},
{
key_as_string: '2014-10-03T00:00:00.000Z',
key: 1412294400000,
doc_count: 7,
agg_1: { value: 220943 },
agg_2: { value: 1412294400000 },
agg_5: { value: 1 },
agg_6: {
hits: {
total: 2,
hits: [{
fields: {
bytes: 553
}
}]
}
}
}
]
}
}
]
}
}
};
data.oneRangeBucket = {
'took': 35,
'timed_out': false,

View file

@ -1,5 +1,6 @@
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import fixtures from 'fixtures/fake_hierarchical_data';
@ -14,12 +15,14 @@ describe('AggTable Directive', function () {
let tabifyAggResponse;
let Vis;
let indexPattern;
let settings;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private) {
beforeEach(ngMock.inject(function ($injector, Private, config) {
tabifyAggResponse = Private(AggResponseTabifyTabifyProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
Vis = Private(VisProvider);
settings = config;
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
@ -107,6 +110,99 @@ describe('AggTable Directive', function () {
});
});
describe('renders totals row', function () {
function totalsRowTest(totalFunc, expected) {
const vis = new Vis(indexPattern, {
type: 'table',
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'min', schema: 'metric', params: { field: '@timestamp' } },
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{ type: 'date_histogram', schema: 'bucket', params: { field: '@timestamp', interval: 'd' } },
{ type: 'derivative', schema: 'metric', params: { metricAgg: 'custom', customMetric: { id:'5-orderAgg', type: 'count' } } },
{ type: 'top_hits', schema: 'metric', params: { field: 'bytes', aggregate: { val: 'min' }, size: 1 } }
]
});
vis.aggs.forEach(function (agg, i) {
agg.id = 'agg_' + (i + 1);
});
function setDefaultTimezone() {
moment.tz.setDefault(settings.get('dateFormat:tz'));
}
const off = $scope.$on('change:config.dateFormat:tz', setDefaultTimezone);
const oldTimezoneSetting = settings.get('dateFormat:tz');
settings.set('dateFormat:tz', 'UTC');
$scope.table = tabifyAggResponse(vis,
fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative,
{ canSplit: false, minimalColumns: true, asAggConfigResults: true }
);
$scope.showTotal = true;
$scope.totalFunc = totalFunc;
const $el = $('<kbn-agg-table table="table" show-total="showTotal" total-func="totalFunc"></kbn-agg-table>');
$compile($el)($scope);
$scope.$digest();
expect($el.find('tfoot').size()).to.be(1);
const $rows = $el.find('tfoot tr');
expect($rows.size()).to.be(1);
const $cells = $($rows[0]).find('th');
expect($cells.size()).to.be(6);
for (let i = 0; i < 6; i++) {
expect($($cells[i]).text()).to.be(expected[i]);
}
settings.set('dateFormat:tz', oldTimezoneSetting);
off();
}
it('as count', function () {
totalsRowTest('count', ['18', '18', '18', '18', '18', '18']);
});
it('as min', function () {
totalsRowTest('min', [
'',
'September 28th 2014, 00:00:00.000',
'9,283',
'September 28th 2014, 00:00:00.000',
'1',
'11'
]);
});
it('as max', function () {
totalsRowTest('max', [
'',
'October 3rd 2014, 00:00:00.000',
'220,943',
'October 3rd 2014, 00:00:00.000',
'239',
'837'
]);
});
it('as avg', function () {
totalsRowTest('avg', [
'',
'',
'87,221.5',
'',
'64.667',
'206.833'
]);
});
it('as sum', function () {
totalsRowTest('sum', [
'',
'',
'1,569,987',
'',
'1,164',
'3,723'
]);
});
});
describe('aggTable.toCsv()', function () {
it('escapes and formats the rows and columns properly', function () {
const $el = $compile('<kbn-agg-table table="table">')($scope);

View file

@ -4,10 +4,13 @@ import 'ui/agg_table/agg_table.less';
import _ from 'lodash';
import uiModules from 'ui/modules';
import aggTableTemplate from 'ui/agg_table/agg_table.html';
import RegistryFieldFormatsProvider from 'ui/registry/field_formats';
uiModules
.get('kibana')
.directive('kbnAggTable', function ($filter, config, Private, compileRecursiveDirective) {
const fieldFormats = Private(RegistryFieldFormatsProvider);
const numberFormatter = fieldFormats.getDefaultInstance('number').getConverterFor('html');
return {
restrict: 'E',
@ -95,29 +98,59 @@ uiModules
formattedColumn.class = 'visualize-table-right';
}
const isFieldNumeric = (field && field.type === 'number');
const isFirstValueNumeric = _.isNumber(_.get(table, `rows[0][${i}].value`));
if (isFieldNumeric || isFirstValueNumeric) {
function sum(tableRows) {
return _.reduce(tableRows, function (prev, curr) {return prev + curr[i].value; }, 0);
let isFieldNumeric = false;
let isFieldDate = false;
const aggType = agg.type;
if (aggType && aggType.type === 'metrics') {
if (aggType.name === 'top_hits') {
if (agg._opts.params.aggregate !== 'concat') {
// all other aggregate types for top_hits output numbers
// so treat this field as numeric
isFieldNumeric = true;
}
} else if (field) {
// if the metric has a field, check if it is either number or date
isFieldNumeric = field.type === 'number';
isFieldDate = field.type === 'date';
} else {
// if there is no field, then it is count or similar so just say number
isFieldNumeric = true;
}
} else if (field) {
isFieldNumeric = field.type === 'number';
isFieldDate = field.type === 'date';
}
if (isFieldNumeric || isFieldDate || $scope.totalFunc === 'count') {
function sum(tableRows) {
return _.reduce(tableRows, function (prev, curr) {
// some metrics return undefined for some of the values
// derivative is an example of this as it returns undefined in the first row
if (curr[i].value === undefined) return prev;
return prev + curr[i].value;
}, 0);
}
const formatter = agg.fieldFormatter('html');
switch ($scope.totalFunc) {
case 'sum':
formattedColumn.total = sum(table.rows);
if (!isFieldDate) {
formattedColumn.total = formatter(sum(table.rows));
}
break;
case 'avg':
formattedColumn.total = sum(table.rows) / table.rows.length;
if (!isFieldDate) {
formattedColumn.total = formatter(sum(table.rows) / table.rows.length);
}
break;
case 'min':
formattedColumn.total = _.chain(table.rows).map(i).map('value').min().value();
formattedColumn.total = formatter(_.chain(table.rows).map(i).map('value').min().value());
break;
case 'max':
formattedColumn.total = _.chain(table.rows).map(i).map('value').max().value();
formattedColumn.total = formatter(_.chain(table.rows).map(i).map('value').max().value());
break;
case 'count':
formattedColumn.total = table.rows.length;
formattedColumn.total = numberFormatter(table.rows.length);
break;
default:
break;

View file

@ -28,7 +28,7 @@
<tbody kbn-rows="page" kbn-rows-min="paginatedTable.rowsToShow(perPage, page.length)"></tbody>
<tfoot ng-if="showTotal">
<tr>
<th ng-repeat="col in columns" class="numeric-value">{{col.total | number}}</th>
<th ng-repeat="col in columns" class="numeric-value">{{col.total}}</th>
</tr>
</tfoot>
</table>