simplified tabify (#19061)

This commit is contained in:
Peter Pisljar 2018-08-30 14:56:18 +02:00 committed by GitHub
parent f6a3f900b1
commit e5a94e7a10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
66 changed files with 682 additions and 1375 deletions

View file

@ -110,6 +110,7 @@ export default function GaugeVisType(Private) {
aggFilter: ['!geohash_grid', '!filter']
}
])
}
},
useCustomNoDataScreen: true
});
}

View file

@ -105,6 +105,7 @@ export default function GoalVisType(Private) {
aggFilter: ['!geohash_grid', '!filter']
}
])
}
},
useCustomNoDataScreen: true
});
}

View file

@ -35,7 +35,7 @@ import 'ui/query_bar';
import { hasSearchStategyForIndexPattern, isDefaultTypeIndexPattern } from 'ui/courier';
import { toastNotifications } from 'ui/notify';
import { VisProvider } from 'ui/vis';
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
import { VislibResponseHandlerProvider } from 'ui/vis/response_handlers/vislib';
import { DocTitleProvider } from 'ui/doc_title';
import PluginsKibanaDiscoverHitSortFnProvider from '../_hit_sort_fn';
import { FilterBarQueryFilterProvider } from 'ui/filter_bar/query_filter';
@ -156,7 +156,7 @@ function discoverController(
const docTitle = Private(DocTitleProvider);
const HitSortFn = Private(PluginsKibanaDiscoverHitSortFnProvider);
const queryFilter = Private(FilterBarQueryFilterProvider);
const responseHandler = Private(BasicResponseHandlerProvider).handler;
const responseHandler = Private(VislibResponseHandlerProvider).handler;
const filterManager = Private(FilterManagerProvider);
const notify = new Notifier({
location: 'Discover'

View file

@ -25,7 +25,7 @@ import { VisProvider } from 'ui/vis';
import LogstashIndexPatternStubProvider from 'fixtures/stubbed_logstash_index_pattern';
import MetricVisProvider from '../metric_vis';
describe('metric_vis', () => {
describe('metric vis', () => {
let setup = null;
let vis;
@ -62,10 +62,8 @@ describe('metric_vis', () => {
const ip = '235.195.237.208';
render({
tables: [{
columns: [{ title: 'ip', aggConfig: vis.aggs[0] }],
rows: [[ ip ]]
}]
columns: [{ id: 'col-0', title: 'ip', aggConfig: vis.aggs[0] }],
rows: [{ 'col-0': ip }]
});
const $link = $(el)

View file

@ -20,7 +20,7 @@
import expect from 'expect.js';
import { MetricVisComponent } from '../metric_vis_controller';
describe('metric vis', function () {
describe('metric vis controller', function () {
const vis = {
params: {
@ -53,10 +53,8 @@ describe('metric vis', function () {
it('should set the metric label and value', function () {
const metrics = metricController._processTableGroups({
tables: [{
columns: [{ title: 'Count', aggConfig: { ...aggConfig, makeLabel: () => 'Count' } }],
rows: [[ 4301021 ]]
}]
columns: [{ id: 'col-0', title: 'Count', aggConfig: { ...aggConfig, makeLabel: () => 'Count' } }],
rows: [{ 'col-0': 4301021 }]
});
expect(metrics.length).to.be(1);
@ -66,13 +64,11 @@ describe('metric vis', function () {
it('should support multi-value metrics', function () {
const metrics = metricController._processTableGroups({
tables: [{
columns: [
{ aggConfig: { ...aggConfig, makeLabel: () => '1st percentile of bytes' } },
{ aggConfig: { ...aggConfig, makeLabel: () => '99th percentile of bytes' } }
],
rows: [[ 182, 445842.4634666484 ]]
}]
columns: [
{ id: 'col-0', aggConfig: { ...aggConfig, makeLabel: () => '1st percentile of bytes' } },
{ id: 'col-1', aggConfig: { ...aggConfig, makeLabel: () => '99th percentile of bytes' } }
],
rows: [{ 'col-0': 182, 'col-1': 445842.4634666484 }]
});
expect(metrics.length).to.be(2);

View file

@ -100,6 +100,7 @@ function MetricVisProvider(Private) {
}
])
},
responseHandler: 'tabify',
});
}

View file

@ -89,7 +89,7 @@ export class MetricVisComponent extends Component {
return fieldFormatter(value);
}
_processTableGroups(tableGroups) {
_processTableGroups(table) {
const config = this.props.vis.params.metric;
const isPercentageMode = config.percentageMode;
const min = config.colorsRange[0].from;
@ -98,56 +98,55 @@ export class MetricVisComponent extends Component {
const labels = this._getLabels();
const metrics = [];
tableGroups.tables.forEach((table, tableIndex) => {
let bucketAgg;
let rowHeaderIndex;
let bucketAgg;
let bucketColumnId;
let rowHeaderIndex;
table.columns.forEach((column, columnIndex) => {
const aggConfig = column.aggConfig;
table.columns.forEach((column, columnIndex) => {
const aggConfig = column.aggConfig;
if (aggConfig && aggConfig.type.type === 'buckets') {
bucketAgg = aggConfig;
// Store the current index, so we later know in which position in the
// row array, the bucket agg key will be, so we can create filters on it.
rowHeaderIndex = columnIndex;
return;
if (aggConfig && aggConfig.type.type === 'buckets') {
bucketAgg = aggConfig;
// Store the current index, so we later know in which position in the
// row array, the bucket agg key will be, so we can create filters on it.
rowHeaderIndex = columnIndex;
bucketColumnId = column.id;
return;
}
table.rows.forEach((row, rowIndex) => {
let title = column.name;
let value = row[column.id];
const color = this._getColor(value, labels, colors);
if (isPercentageMode) {
const percentage = Math.round(100 * (value - min) / (max - min));
value = `${percentage}%`;
}
table.rows.forEach((row, rowIndex) => {
let title = column.title;
let value = row[columnIndex];
const color = this._getColor(value, labels, colors);
if (isPercentageMode) {
const percentage = Math.round(100 * (value - min) / (max - min));
value = `${percentage}%`;
if (aggConfig) {
if (!isPercentageMode) value = this._getFormattedValue(aggConfig.fieldFormatter('html'), value);
if (bucketAgg) {
const bucketValue = bucketAgg.fieldFormatter('text')(row[bucketColumnId]);
title = `${bucketValue} - ${aggConfig.makeLabel()}`;
} else {
title = aggConfig.makeLabel();
}
}
if (aggConfig) {
if (!isPercentageMode) value = this._getFormattedValue(aggConfig.fieldFormatter('html'), value);
if (bucketAgg) {
const bucketValue = bucketAgg.fieldFormatter('text')(row[0]);
title = `${bucketValue} - ${aggConfig.makeLabel()}`;
} else {
title = aggConfig.makeLabel();
}
}
const shouldColor = config.colorsRange.length > 1;
const shouldColor = config.colorsRange.length > 1;
metrics.push({
label: title,
value: value,
color: shouldColor && config.style.labelColor ? color : null,
bgColor: shouldColor && config.style.bgColor ? color : null,
lightText: shouldColor && config.style.bgColor && this._needsLightText(color),
filterKey: rowHeaderIndex !== undefined ? row[rowHeaderIndex] : null,
tableIndex: tableIndex,
rowIndex: rowIndex,
columnIndex: rowHeaderIndex,
bucketAgg: bucketAgg,
});
metrics.push({
label: title,
value: value,
color: shouldColor && config.style.labelColor ? color : null,
bgColor: shouldColor && config.style.bgColor ? color : null,
lightText: shouldColor && config.style.bgColor && this._needsLightText(color),
filterKey: bucketColumnId !== undefined ? row[bucketColumnId] : null,
rowIndex: rowIndex,
columnIndex: rowHeaderIndex,
bucketAgg: bucketAgg,
});
});
});
@ -159,7 +158,7 @@ export class MetricVisComponent extends Component {
if (!metric.filterKey || !metric.bucketAgg) {
return;
}
const table = this.props.visData.tables[metric.tableIndex];
const table = this.props.visData;
this.props.vis.API.events.addFilter(table, metric.columnIndex, metric.rowIndex);
};

View file

@ -108,22 +108,26 @@ describe('RegionMapsVisualizationTests', function () {
const _makeJsonAjaxCallOld = ChoroplethLayer.prototype._makeJsonAjaxCall;
const dummyTableGroup = {
tables: [
{
columns: [{
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' }
}, 'title': 'geo.dest: Descending'
}, {
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [['CN', 26], ['IN', 17], ['US', 6], ['DE', 4], ['BR', 3]]
}
columns: [{
'id': 'col-0',
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' }
}, 'title': 'geo.dest: Descending'
}, {
'id': 'col-1',
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [
{ 'col-0': 'CN', 'col-1': 26 },
{ 'col-0': 'IN', 'col-1': 17 },
{ 'col-0': 'US', 'col-1': 6 },
{ 'col-0': 'DE', 'col-1': 4 },
{ 'col-0': 'BR', 'col-1': 3 }
]
};
@ -293,7 +297,7 @@ describe('RegionMapsVisualizationTests', function () {
});
const newTableGroup = _.cloneDeep(dummyTableGroup);
newTableGroup.tables[0].rows.pop();//remove one shape
newTableGroup.rows.pop();//remove one shape
await regionMapsVisualization.render(newTableGroup, {
resize: false,
@ -306,7 +310,7 @@ describe('RegionMapsVisualizationTests', function () {
const anotherTableGroup = _.cloneDeep(newTableGroup);
anotherTableGroup.tables[0].rows.pop();//remove one shape
anotherTableGroup.rows.pop();//remove one shape
domNode.style.width = '412px';
domNode.style.height = '112px';
await regionMapsVisualization.render(anotherTableGroup, {
@ -336,7 +340,7 @@ describe('RegionMapsVisualizationTests', function () {
});
const newTableGroup = _.cloneDeep(dummyTableGroup);
newTableGroup.tables[0].rows.pop();//remove one shape
newTableGroup.rows.pop();//remove one shape
vis.params.colorSchema = 'Blues';
await regionMapsVisualization.render(newTableGroup, {
resize: false,

View file

@ -46,14 +46,17 @@ export function RegionMapsVisualizationProvider(Private, config) {
}
}
async _updateData(tableGroup) {
this._chartData = tableGroup;
async _updateData(table) {
this._chartData = table;
let results;
if (!tableGroup || !tableGroup.tables || !tableGroup.tables.length || tableGroup.tables[0].columns.length !== 2) {
if (!table || !table.rows.length || table.columns.length !== 2) {
results = [];
} else {
const buckets = tableGroup.tables[0].rows;
results = buckets.map(([term, value]) => {
const termColumn = table.columns[0].id;
const valueColumn = table.columns[1].id;
results = table.rows.map(row => {
const term = row[termColumn];
const value = row[valueColumn];
return { term: term, value: value };
});
}
@ -150,8 +153,8 @@ export function RegionMapsVisualizationProvider(Private, config) {
return;
}
const rowIndex = this._chartData.tables[0].rows.findIndex(row => row[0] === event);
this._vis.API.events.addFilter(this._chartData.tables[0], 0, rowIndex, event);
const rowIndex = this._chartData.rows.findIndex(row => row[0] === event);
this._vis.API.events.addFilter(this._chartData, 0, rowIndex, event);
});
this._choroplethLayer.on('styleChanged', (event) => {

View file

@ -21,7 +21,7 @@ import $ from 'jquery';
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { LegacyResponseHandlerProvider } from 'ui/vis/response_handlers/legacy';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { AppStateProvider } from 'ui/state_management/app_state';
@ -35,6 +35,7 @@ describe('Table Vis Controller', function () {
let Vis;
let fixtures;
let AppState;
let tableAggResponse;
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
beforeEach(ngMock.inject(function ($injector) {
@ -44,6 +45,7 @@ describe('Table Vis Controller', function () {
fixtures = require('fixtures/fake_hierarchical_data');
AppState = Private(AppStateProvider);
Vis = Private(VisProvider);
tableAggResponse = Private(LegacyResponseHandlerProvider).handler;
}));
function OneRangeVis(params) {
@ -99,16 +101,14 @@ describe('Table Vis Controller', function () {
$rootScope.$apply();
}
it('exposes #tableGroups and #hasSomeRows when a response is attached to scope', function () {
it('exposes #tableGroups and #hasSomeRows when a response is attached to scope', async function () {
const vis = new OneRangeVis();
initController(vis);
expect(!$scope.tableGroups).to.be.ok();
expect(!$scope.hasSomeRows).to.be.ok();
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig(), fixtures.oneRangeBucket, {
isHierarchical: vis.isHierarchical()
}));
attachEsResponseToScope(await tableAggResponse(vis, fixtures.oneRangeBucket));
expect($scope.hasSomeRows).to.be(true);
expect($scope.tableGroups).to.have.property('tables');
@ -117,20 +117,18 @@ describe('Table Vis Controller', function () {
expect($scope.tableGroups.tables[0].rows).to.have.length(2);
});
it('clears #tableGroups and #hasSomeRows when the response is removed', function () {
it('clears #tableGroups and #hasSomeRows when the response is removed', async function () {
const vis = new OneRangeVis();
initController(vis);
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig(), fixtures.oneRangeBucket, {
isHierarchical: vis.isHierarchical()
}));
attachEsResponseToScope(await tableAggResponse(vis, fixtures.oneRangeBucket));
removeEsResponseFromScope();
expect(!$scope.hasSomeRows).to.be.ok();
expect(!$scope.tableGroups).to.be.ok();
});
it('sets the sort on the scope when it is passed as a vis param', function () {
it('sets the sort on the scope when it is passed as a vis param', async function () {
const sortObj = {
columnIndex: 1,
direction: 'asc'
@ -142,15 +140,13 @@ describe('Table Vis Controller', function () {
const resp = _.cloneDeep(fixtures.oneRangeBucket);
resp.aggregations.agg_2.buckets = {};
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig(), resp, {
isHierarchical: vis.isHierarchical()
}));
attachEsResponseToScope(await tableAggResponse(vis, resp));
expect($scope.sort.columnIndex).to.equal(sortObj.columnIndex);
expect($scope.sort.direction).to.equal(sortObj.direction);
});
it('sets #hasSomeRows properly if the table group is empty', function () {
it('sets #hasSomeRows properly if the table group is empty', async function () {
const vis = new OneRangeVis();
initController(vis);
@ -158,9 +154,7 @@ describe('Table Vis Controller', function () {
const resp = _.cloneDeep(fixtures.oneRangeBucket);
resp.aggregations.agg_2.buckets = {};
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig(), resp, {
isHierarchical: vis.isHierarchical()
}));
attachEsResponseToScope(await tableAggResponse(vis, resp));
expect($scope.hasSomeRows).to.be(false);
expect(!$scope.tableGroups).to.be.ok();

View file

@ -96,6 +96,7 @@ function TableVisTypeProvider(Private) {
}
])
},
responseHandler: 'legacy',
responseHandlerConfig: {
asAggConfigResults: true
},

View file

@ -39,23 +39,27 @@ describe('TagCloudVisualizationTest', function () {
let imageComparator;
const dummyTableGroup = {
tables: [
{
columns: [{
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' },
fieldFormatter: () => (x => x)
}, 'title': 'geo.dest: Descending'
}, {
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [['CN', 26], ['IN', 17], ['US', 6], ['DE', 4], ['BR', 3]]
}
columns: [{
id: 'col-0',
'aggConfig': {
'id': '2',
'enabled': true,
'type': 'terms',
'schema': 'segment',
'params': { 'field': 'geo.dest', 'size': 5, 'order': 'desc', 'orderBy': '1' },
fieldFormatter: () => (x => x)
}, 'title': 'geo.dest: Descending'
}, {
id: 'col-1',
'aggConfig': { 'id': '1', 'enabled': true, 'type': 'count', 'schema': 'metric', 'params': {} },
'title': 'Count'
}],
rows: [
{ 'col-0': 'CN', 'col-1': 26 },
{ 'col-0': 'IN', 'col-1': 17 },
{ 'col-0': 'US', 'col-1': 6 },
{ 'col-0': 'DE', 'col-1': 4 },
{ 'col-0': 'BR', 'col-1': 3 }
]
};

View file

@ -77,6 +77,7 @@ VisTypesRegistryProvider.register(function (Private) {
aggFilter: ['terms', 'significant_terms']
}
])
}
},
useCustomNoDataScreen: true
});
});

View file

@ -110,13 +110,12 @@ export class TagCloudVisualization {
}
_updateData(response) {
if (!response || !response.tables.length) {
_updateData(data) {
if (!data || !data.rows.length) {
this._tagCloud.setData([]);
return;
}
const data = response.tables[0];
const segmentAggs = this._vis.aggs.bySchemaName.segment;
if (segmentAggs && segmentAggs.length > 0) {
this._bucketAgg = segmentAggs[0];
@ -124,12 +123,16 @@ export class TagCloudVisualization {
this._bucketAgg = null;
}
const hasTags = data.columns.length === 2;
const tagColumn = hasTags ? data.columns[0].id : -1;
const metricColumn = data.columns[hasTags ? 1 : 0].id;
const tags = data.rows.map((row, rowIndex) => {
const [tag, count] = row;
const tag = row[tagColumn] || 'all';
const metric = row[metricColumn];
return {
displayText: this._bucketAgg ? this._bucketAgg.fieldFormatter()(tag) : tag,
rawText: tag,
value: count,
value: metric,
meta: {
data: data,
rowIndex: rowIndex,

View file

@ -34,9 +34,7 @@ export function makeGeoJsonResponseHandler() {
//double conversion, first to table, then to geojson
//This is to future-proof this code for Canvas-refactoring
const tabifiedResponse = tabifyAggResponse(vis.getAggConfig(), esResponse, {
asAggConfigResults: false
});
const tabifiedResponse = tabifyAggResponse(vis.getAggConfig(), esResponse);
lastGeoJsonResponse = convertToGeoJson(tabifiedResponse);
return lastGeoJsonResponse;

View file

@ -43,22 +43,18 @@ describe('makeFakeXAspect', function () {
expect(aspect)
.to.have.property('i', -1)
.and.have.property('agg')
.and.have.property('col');
.and.have.property('aggConfig')
.and.have.property('title');
expect(aspect.agg)
expect(aspect.aggConfig)
.to.be.an(AggConfig)
.and.to.have.property('type');
expect(aspect.agg.type)
expect(aspect.aggConfig.type)
.to.be.an(AggType)
.and.to.have.property('name', 'all')
.and.to.have.property('title', 'All docs')
.and.to.have.property('hasNoDsl', true);
expect(aspect.col)
.to.be.an('object')
.and.to.have.property('aggConfig', aspect.agg)
.and.to.have.property('label', aspect.agg.makeLabel());
});
});

View file

@ -58,8 +58,7 @@ describe('getAspects', function () {
expect(aspect)
.to.be.an('object')
.and.have.property('i', i)
.and.have.property('agg', vis.aggs[i])
.and.have.property('col', table.columns[i]);
.and.have.property('aggConfig', vis.aggs[i]);
}
function init(group, x, y) {
@ -150,13 +149,10 @@ describe('getAspects', function () {
expect(aspects.x)
.to.be.an('object')
.and.have.property('i', -1)
.and.have.property('agg')
.and.have.property('col');
.and.have.property('aggConfig')
.and.have.property('title');
expect(aspects.x.agg).to.be.an(AggConfig);
expect(aspects.x.col)
.to.be.an('object')
.and.to.have.property('aggConfig', aspects.x.agg);
expect(aspects.x.aggConfig).to.be.an(AggConfig);
});
});

View file

@ -37,15 +37,13 @@ describe('getPoint', function () {
describe('Without series aspect', function () {
let seriesAspect;
let xAspect;
let yCol;
let yAspect;
let yScale;
beforeEach(function () {
seriesAspect = null;
xAspect = { i: 0 };
yCol = { title: 'Y', aggConfig: {} };
yAspect = { i: 1, col: yCol };
yAspect = { i: 1, title: 'Y', aggConfig: {} };
yScale = 5;
});
@ -58,7 +56,7 @@ describe('getPoint', function () {
.to.have.property('x', 1)
.and.have.property('y', 10)
.and.have.property('z', 3)
.and.have.property('series', yCol.title)
.and.have.property('series', yAspect.title)
.and.have.property('aggConfigResult', row[1]);
});
@ -83,7 +81,7 @@ describe('getPoint', function () {
});
it('properly unwraps and scales values', function () {
const seriesAspect = { i: 1, agg: identFormatted };
const seriesAspect = { i: 1, aggConfig: identFormatted };
const point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point)
@ -94,7 +92,7 @@ describe('getPoint', function () {
});
it('properly formats series values', function () {
const seriesAspect = { i: 1, agg: truthFormatted };
const seriesAspect = { i: 1, aggConfig: truthFormatted };
const point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point)
@ -105,7 +103,7 @@ describe('getPoint', function () {
});
it ('adds the aggConfig to the points', function () {
const seriesAspect = { i: 1, agg: truthFormatted };
const seriesAspect = { i: 1, aggConfig: truthFormatted };
const point = getPoint(xAspect, seriesAspect, yScale, row, yAspect);
expect(point).to.have.property('aggConfig', truthFormatted);

View file

@ -47,11 +47,10 @@ describe('getSeries', function () {
[1, 2, 3]
].map(wrapRows);
const yCol = { aggConfig: {}, title: 'y' };
const chart = {
aspects: {
x: { i: 0 },
y: { i: 1, col: yCol, agg: { id: 'id' } },
y: { i: 1, title: 'y', aggConfig: { id: 'id' } },
z: { i: 2 }
}
};
@ -65,7 +64,7 @@ describe('getSeries', function () {
const siri = series[0];
expect(siri)
.to.be.an('object')
.and.have.property('label', yCol.title)
.and.have.property('label', chart.aspects.y.title)
.and.have.property('values');
expect(siri.values)
@ -93,8 +92,8 @@ describe('getSeries', function () {
aspects: {
x: { i: 0 },
y: [
{ i: 1, col: { title: '0' }, agg: { id: 1 } },
{ i: 2, col: { title: '1' }, agg: { id: 2 } },
{ i: 1, title: '0', aggConfig: { id: 1 } },
{ i: 2, title: '1', aggConfig: { id: 2 } },
]
}
};
@ -138,8 +137,8 @@ describe('getSeries', function () {
const chart = {
aspects: {
x: { i: -1 },
series: { i: 0, agg: agg },
y: { i: 1, col: { title: '0' }, agg: agg }
series: { i: 0, aggConfig: agg },
y: { i: 1, title: '0', aggConfig: agg }
}
};
@ -180,10 +179,10 @@ describe('getSeries', function () {
const chart = {
aspects: {
x: { i: -1 },
series: { i: 0, agg: agg },
series: { i: 0, aggConfig: agg },
y: [
{ i: 1, col: { title: '0' }, agg: { id: 1 } },
{ i: 2, col: { title: '1' }, agg: { id: 2 } }
{ i: 1, title: '0', aggConfig: { id: 1 } },
{ i: 2, title: '1', aggConfig: { id: 2 } }
]
}
};
@ -230,10 +229,10 @@ describe('getSeries', function () {
const chart = {
aspects: {
x: { i: -1 },
series: { i: 0, agg: agg },
series: { i: 0, aggConfig: agg },
y: [
{ i: 1, col: { title: '0' }, agg: { id: 1 } },
{ i: 2, col: { title: '1' }, agg: { id: 2 } }
{ i: 1, title: '0', aggConfig: { id: 1 } },
{ i: 2, title: '1', aggConfig: { id: 2 } }
]
}
};

View file

@ -34,14 +34,12 @@ describe('initXAxis', function () {
const baseChart = {
aspects: {
x: {
agg: {
aggConfig: {
fieldFormatter: _.constant({}),
write: _.constant({ params: {} }),
type: {}
},
col: {
title: 'label'
}
title: 'label'
}
}
};
@ -53,23 +51,23 @@ describe('initXAxis', function () {
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x.agg.fieldFormatter());
.and.have.property('xAxisFormatter', chart.aspects.x.aggConfig.fieldFormatter());
});
it('makes the chart ordered if the agg is ordered', function () {
const chart = _.cloneDeep(baseChart);
chart.aspects.x.agg.type.ordered = true;
chart.aspects.x.agg.params = {
chart.aspects.x.aggConfig.type.ordered = true;
chart.aspects.x.aggConfig.params = {
field: field
};
chart.aspects.x.agg.vis = {
chart.aspects.x.aggConfig.vis = {
indexPattern: indexPattern
};
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x.agg.fieldFormatter())
.and.have.property('xAxisFormatter', chart.aspects.x.aggConfig.fieldFormatter())
.and.have.property('indexPattern', indexPattern)
.and.have.property('xAxisField', field)
.and.have.property('ordered');
@ -81,19 +79,19 @@ describe('initXAxis', function () {
it('reads the interval param from the x agg', function () {
const chart = _.cloneDeep(baseChart);
chart.aspects.x.agg.type.ordered = true;
chart.aspects.x.agg.write = _.constant({ params: { interval: 10 } });
chart.aspects.x.agg.params = {
chart.aspects.x.aggConfig.type.ordered = true;
chart.aspects.x.aggConfig.write = _.constant({ params: { interval: 10 } });
chart.aspects.x.aggConfig.params = {
field: field
};
chart.aspects.x.agg.vis = {
chart.aspects.x.aggConfig.vis = {
indexPattern: indexPattern
};
initXAxis(chart);
expect(chart)
.to.have.property('xAxisLabel', 'label')
.and.have.property('xAxisFormatter', chart.aspects.x.agg.fieldFormatter())
.and.have.property('xAxisFormatter', chart.aspects.x.aggConfig.fieldFormatter())
.and.have.property('indexPattern', indexPattern)
.and.have.property('xAxisField', field)
.and.have.property('ordered');

View file

@ -42,12 +42,12 @@ describe('initYAxis', function () {
const baseChart = {
aspects: {
y: [
{ agg: agg(), col: { title: 'y1' } },
{ agg: agg(), col: { title: 'y2' } },
{ aggConfig: agg(), title: 'y1' },
{ aggConfig: agg(), title: 'y2' },
],
x: {
agg: agg(),
col: { title: 'x' }
aggConfig: agg(),
title: 'x'
}
}
};
@ -59,7 +59,7 @@ describe('initYAxis', function () {
it('sets the yAxisFormatter the the field formats convert fn', function () {
const chart = _.cloneDeep(singleYBaseChart);
initYAxis(chart);
expect(chart).to.have.property('yAxisFormatter', chart.aspects.y.agg.fieldFormatter());
expect(chart).to.have.property('yAxisFormatter', chart.aspects.y.aggConfig.fieldFormatter());
});
it('sets the yAxisLabel', function () {
@ -76,8 +76,8 @@ describe('initYAxis', function () {
expect(chart).to.have.property('yAxisFormatter');
expect(chart.yAxisFormatter)
.to.be(chart.aspects.y[0].agg.fieldFormatter())
.and.not.be(chart.aspects.y[1].agg.fieldFormatter());
.to.be(chart.aspects.y[0].aggConfig.fieldFormatter())
.and.not.be(chart.aspects.y[1].aggConfig.fieldFormatter());
});
it('does not set the yAxisLabel, it does not make sense to put multiple labels on the same axis', function () {

View file

@ -23,7 +23,6 @@ import AggConfigResult from '../../../vis/agg_config_result';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { VisProvider } from '../../../vis';
import { TabifyTable } from '../../tabify/_table';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { AggResponsePointSeriesProvider } from '../point_series';
@ -47,7 +46,7 @@ describe('pointSeriesChartDataFromTable', function () {
const agg = vis.aggs[0];
const result = new AggConfigResult(vis.aggs[0], void 0, 100, 100);
const table = new TabifyTable();
const table = { rows: [] };
table.columns = [ { aggConfig: agg } ];
table.rows.push([ result ]);
@ -86,7 +85,7 @@ describe('pointSeriesChartDataFromTable', function () {
};
const rowCount = 3;
const table = new TabifyTable();
const table = { rows: [] };
table.columns = [ x.col, y.col ];
_.times(rowCount, function (i) {
const date = new AggConfigResult(x.agg, void 0, x.at(i));
@ -147,7 +146,7 @@ describe('pointSeriesChartDataFromTable', function () {
};
const rowCount = 3;
const table = new TabifyTable();
const table = { rows: [] };
table.columns = [ date.col, avg.col, max.col ];
_.times(rowCount, function (i) {
const dateResult = new AggConfigResult(date.agg, void 0, date.at(i));
@ -226,7 +225,7 @@ describe('pointSeriesChartDataFromTable', function () {
const metricCount = 2;
const rowsPerSegment = 2;
const rowCount = extensions.length * rowsPerSegment;
const table = new TabifyTable();
const table = { rows: [] };
table.columns = [ date.col, term.col, avg.col, max.col ];
_.times(rowCount, function (i) {
const dateResult = new AggConfigResult(date.agg, void 0, date.at(i));

View file

@ -35,7 +35,7 @@ describe('orderedDateAxis', function () {
chart: {
aspects: {
x: {
agg: {
aggConfig: {
fieldIsTimeField: _.constant(true),
buckets: {
getScaledDateFormat: _.constant('hh:mm:ss'),
@ -88,7 +88,7 @@ describe('orderedDateAxis', function () {
it('relies on agg.buckets for the interval', function () {
const args = _.cloneDeep(baseArgs);
const spy = sinon.spy(args.chart.aspects.x.agg.buckets, 'getInterval');
const spy = sinon.spy(args.chart.aspects.x.aggConfig.buckets, 'getInterval');
orderedDateAxis(args.vis, args.chart);
expect(spy).to.have.property('callCount', 1);
});
@ -102,7 +102,7 @@ describe('orderedDateAxis', function () {
it('does not set the min/max when the buckets are unbounded', function () {
const args = _.cloneDeep(baseArgs);
args.chart.aspects.x.agg.buckets.getBounds = _.constant();
args.chart.aspects.x.aggConfig.buckets.getBounds = _.constant();
orderedDateAxis(args.vis, args.chart);
expect(args.chart.ordered).to.not.have.property('min');
expect(args.chart.ordered).to.not.have.property('max');

View file

@ -17,15 +17,16 @@
* under the License.
*/
import './_main';
import './_add_to_siri';
import './_fake_x_aspect';
import './_get_aspects';
import './_get_point';
import './_get_series';
import './_init_x_axis';
import './_init_y_axis';
import './_ordered_date_axis';
import './_tooltip_formatter';
describe('Point Series Agg Response', function () {
require ('./_main');
require('./_add_to_siri');
require('./_fake_x_aspect');
require('./_get_aspects');
require('./_get_point');
require('./_get_series');
require('./_init_x_axis');
require('./_init_y_axis');
require('./_ordered_date_axis');
require('./_tooltip_formatter');
});

View file

@ -37,11 +37,8 @@ export function PointSeriesFakeXAxisProvider() {
return {
i: -1,
agg: fake,
col: {
aggConfig: fake,
label: fake.makeLabel()
}
aggConfig: fake,
title: fake.makeLabel(),
};
};
}

View file

@ -39,8 +39,8 @@ export function PointSeriesGetAspectsProvider(Private) {
const aspect = {
i: i,
col: col,
agg: col.aggConfig
title: col.title,
aggConfig: col.aggConfig
};
if (!aspects[name]) aspects[name] = [];

View file

@ -45,13 +45,13 @@ export function PointSeriesGetPointProvider() {
if (series) {
const seriesArray = series.length ? series : [ series ];
point.aggConfig = seriesArray[0].agg;
point.series = seriesArray.map(s => s.agg.fieldFormatter()(unwrap(row[s.i]))).join(' - ');
point.aggConfig = seriesArray[0].aggConfig;
point.series = seriesArray.map(s => s.aggConfig.fieldFormatter()(unwrap(row[s.i]))).join(' - ');
} else if (y) {
// If the data is not split up with a series aspect, then
// each point's "series" becomes the y-agg that produced it
point.aggConfig = y.col.aggConfig;
point.series = y.col.title;
point.aggConfig = y.aggConfig;
point.series = y.title;
}
if (yScale) {

View file

@ -35,7 +35,7 @@ export function PointSeriesGetSeriesProvider(Private) {
.transform(function (series, row) {
if (!multiY) {
const point = partGetPoint(row, aspects.y, aspects.z);
if (point) addToSiri(series, point, point.series, point.series, aspects.y.agg);
if (point) addToSiri(series, point, point.series, point.series, aspects.y.aggConfig);
return;
}
@ -46,8 +46,8 @@ export function PointSeriesGetSeriesProvider(Private) {
// use the point's y-axis as it's series by default,
// but augment that with series aspect if it's actually
// available
let seriesId = y.agg.id;
let seriesLabel = y.col.title;
let seriesId = y.aggConfig.id;
let seriesLabel = y.title;
if (aspects.series) {
const prefix = point.series ? point.series + ': ' : '';
@ -55,7 +55,7 @@ export function PointSeriesGetSeriesProvider(Private) {
seriesLabel = prefix + seriesLabel;
}
addToSiri(series, point, seriesId, seriesLabel, y.agg);
addToSiri(series, point, seriesId, seriesLabel, y.aggConfig);
});
}, new Map())
@ -70,7 +70,7 @@ export function PointSeriesGetSeriesProvider(Private) {
if (firstVal) {
const agg = firstVal.aggConfigResult.aggConfig;
y = _.find(aspects.y, function (y) {
return y.agg === agg;
return y.aggConfig === agg;
});
}

View file

@ -21,16 +21,16 @@
export function PointSeriesInitXAxisProvider() {
return function initXAxis(chart) {
const x = chart.aspects.x;
chart.xAxisFormatter = x.agg ? x.agg.fieldFormatter() : String;
chart.xAxisLabel = x.col.title;
chart.xAxisFormatter = x.aggConfig ? x.aggConfig.fieldFormatter() : String;
chart.xAxisLabel = x.title;
if (!x.agg || !x.agg.type.ordered) return;
if (!x.aggConfig || !x.aggConfig.type.ordered) return;
chart.indexPattern = x.agg.vis.indexPattern;
chart.xAxisField = x.agg.params.field;
chart.indexPattern = x.aggConfig.vis.indexPattern;
chart.xAxisField = x.aggConfig.params.field;
chart.ordered = {};
const xAggOutput = x.agg.write();
const xAggOutput = x.aggConfig.write();
if (xAggOutput.params.interval) {
chart.ordered.interval = xAggOutput.params.interval;
}

View file

@ -24,21 +24,21 @@ export function PointSeriesInitYAxisProvider() {
if (Array.isArray(y)) {
// TODO: vis option should allow choosing this format
chart.yAxisFormatter = y[0].agg.fieldFormatter();
chart.yAxisFormatter = y[0].aggConfig.fieldFormatter();
chart.yAxisLabel = ''; // use the legend
} else {
chart.yAxisFormatter = y.agg.fieldFormatter();
chart.yAxisLabel = y.col.title;
chart.yAxisFormatter = y.aggConfig.fieldFormatter();
chart.yAxisLabel = y.title;
}
const z = chart.aspects.series;
if (z) {
if (Array.isArray(z)) {
chart.zAxisFormatter = z[0].agg.fieldFormatter();
chart.zAxisFormatter = z[0].aggConfig.fieldFormatter();
chart.zAxisLabel = ''; // use the legend
} else {
chart.zAxisFormatter = z.agg.fieldFormatter();
chart.zAxisLabel = z.col.title;
chart.zAxisFormatter = z.aggConfig.fieldFormatter();
chart.zAxisLabel = z.title;
}
}
};

View file

@ -22,7 +22,7 @@ import moment from 'moment';
export function PointSeriesOrderedDateAxisProvider() {
return function orderedDateAxis(vis, chart) {
const xAgg = chart.aspects.x.agg;
const xAgg = chart.aspects.x.aggConfig;
const buckets = xAgg.buckets;
const format = buckets.getScaledDateFormat();

View file

@ -42,7 +42,7 @@ export function AggResponsePointSeriesProvider(Private) {
initXAxis(chart);
initYAxis(chart);
const datedX = aspects.x.agg.type.ordered && aspects.x.agg.type.ordered.date;
const datedX = aspects.x.aggConfig.type.ordered && aspects.x.aggConfig.type.ordered.date;
if (datedX) {
setupOrderedDateXAxis(vis, chart);
}

View file

@ -52,7 +52,7 @@ describe('get columns', function () {
]
});
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical());
expect(columns).to.have.length(2);
expect(columns[1]).to.have.property('aggConfig');
@ -70,7 +70,7 @@ describe('get columns', function () {
]
});
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical());
expect(columns).to.have.length(8);
columns.forEach(function (column, i) {
@ -92,7 +92,7 @@ describe('get columns', function () {
]
});
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical());
function checkColumns(column, i) {
expect(column).to.have.property('aggConfig');
@ -128,7 +128,7 @@ describe('get columns', function () {
]
});
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), !vis.isHierarchical());
expect(columns).to.have.length(6);
// sum should be last

View file

@ -49,16 +49,14 @@ describe('tabifyAggResponse Integration', function () {
normalizeIds(vis);
const resp = tabifyAggResponse(vis.getAggConfig(), fixtures.metricOnly, {
canSplit: false,
isHierarchical: vis.isHierarchical()
metricsAtAllLevels: vis.isHierarchical()
});
expect(resp).to.not.have.property('tables');
expect(resp).to.have.property('rows').and.property('columns');
expect(resp.rows).to.have.length(1);
expect(resp.columns).to.have.length(1);
expect(resp.rows[0]).to.eql([1000]);
expect(resp.rows[0]).to.eql({ 'col-0-agg_1': 1000 });
expect(resp.columns[0]).to.have.property('aggConfig', vis.aggs[0]);
});
@ -94,33 +92,6 @@ describe('tabifyAggResponse Integration', function () {
esResp.aggregations.agg_2.buckets[1].agg_3.buckets[0].agg_4.buckets = [];
});
// check that the root table group is formed properly, then pass
// each table to expectExtensionSplit, along with the expectInnerTables()
// function.
function expectRootGroup(rootTableGroup, expectInnerTables) {
expect(rootTableGroup).to.have.property('tables');
const tables = rootTableGroup.tables;
expect(tables).to.be.an('array').and.have.length(3);
expectExtensionSplit(tables[0], 'png', expectInnerTables);
expectExtensionSplit(tables[1], 'css', expectInnerTables);
expectExtensionSplit(tables[2], 'html', expectInnerTables);
}
// check that the tableGroup for the extension agg was formed properly
// then call expectTable() on each table inside. it should validate that
// each table is formed properly
function expectExtensionSplit(tableGroup, key, expectTable) {
expect(tableGroup).to.have.property('tables');
expect(tableGroup).to.have.property('aggConfig', ext);
expect(tableGroup).to.have.property('key', key);
expect(tableGroup.tables).to.be.an('array').and.have.length(1);
tableGroup.tables.forEach(function (table) {
expectTable(table, key);
});
}
// check that the columns of a table are formed properly
function expectColumns(table, aggs) {
expect(table.columns).to.be.an('array').and.have.length(aggs.length);
@ -131,10 +102,11 @@ describe('tabifyAggResponse Integration', function () {
// check that a row has expected values
function expectRow(row, asserts) {
expect(row).to.be.an('array');
expect(row).to.have.length(asserts.length);
expect(row).to.be.an('object');
asserts.forEach(function (assert, i) {
assert(row[i]);
if (row[`col-${i}`]) {
assert(row[`col-${i}`]);
}
});
}
@ -144,10 +116,10 @@ describe('tabifyAggResponse Integration', function () {
expect(val).to.have.length(2);
}
// check for an empty cell
function expectEmpty(val) {
// check for an OS term
function expectExtension(val) {
expect(val)
.to.be('');
.to.match(/^(js|png|html|css|jpg)$/);
}
// check for an OS term
@ -162,127 +134,44 @@ describe('tabifyAggResponse Integration', function () {
expect(val === 0 || val > 1000).to.be.ok();
}
// create an assert that checks for an expected value
function expectVal(expected) {
return function (val) {
expect(val).to.be(expected);
};
}
it('for non-hierarchical vis', function () {
// the default for a non-hierarchical vis is to display
// only complete rows, and only put the metrics at the end.
vis.isHierarchical = _.constant(false);
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { isHierarchical: vis.isHierarchical() });
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { minimalColumns: true });
expectRootGroup(tabbed, function expectTable(table, splitKey) {
expectColumns(table, [src, os, avg]);
expectColumns(tabbed, [ext, src, os, avg]);
table.rows.forEach(function (row) {
if (splitKey === 'css' && row[0] === 'MX') {
throw new Error('expected the MX row in the css table to be removed');
} else {
expectRow(row, [
expectCountry,
expectOS,
expectAvgBytes
]);
}
});
tabbed.rows.forEach(function (row) {
expectRow(row, [
expectExtension,
expectCountry,
expectOS,
expectAvgBytes
]);
});
});
it('for hierarchical vis, with partial rows', function () {
it('for hierarchical vis', function () {
// since we have partialRows we expect that one row will have some empty
// values, and since the vis is hierarchical and we are NOT using
// minimalColumns we should expect the partial row to be completely after
// the existing bucket and it's metric
vis.isHierarchical = _.constant(true);
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, {
partialRows: true,
isHierarchical: vis.isHierarchical()
});
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, { metricsAtAllLevels: true });
expectRootGroup(tabbed, function expectTable(table, splitKey) {
expectColumns(table, [src, avg, os, avg]);
expectColumns(tabbed, [ext, avg, src, avg, os, avg]);
table.rows.forEach(function (row) {
if (splitKey === 'css' && row[0] === 'MX') {
expectRow(row, [
expectCountry,
expectAvgBytes,
expectEmpty,
expectEmpty
]);
} else {
expectRow(row, [
expectCountry,
expectAvgBytes,
expectOS,
expectAvgBytes
]);
}
});
});
});
it('for hierarchical vis, with partial rows, and minimal columns', function () {
// since we have partialRows we expect that one row has some empty
// values, and since the vis is hierarchical and we are displaying using
// minimalColumns, we should expect the partial row to have a metric at
// the end
vis.isHierarchical = _.constant(true);
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, {
partialRows: true,
minimalColumns: true,
isHierarchical: vis.isHierarchical()
});
expectRootGroup(tabbed, function expectTable(table, splitKey) {
expectColumns(table, [src, os, avg]);
table.rows.forEach(function (row) {
if (splitKey === 'css' && row[0] === 'MX') {
expectRow(row, [
expectCountry,
expectEmpty,
expectVal(9299)
]);
} else {
expectRow(row, [
expectCountry,
expectOS,
expectAvgBytes
]);
}
});
});
});
it('for non-hierarchical vis, minimal columns set to false', function () {
// the reason for this test is mainly to check that setting
// minimalColumns = false on a non-hierarchical vis doesn't
// create metric columns after each bucket
vis.isHierarchical = _.constant(false);
const tabbed = tabifyAggResponse(vis.getAggConfig(), esResp, {
minimalColumns: false,
isHierarchical: vis.isHierarchical()
});
expectRootGroup(tabbed, function expectTable(table) {
expectColumns(table, [src, os, avg]);
table.rows.forEach(function (row) {
expectRow(row, [
expectCountry,
expectOS,
expectAvgBytes
]);
});
tabbed.rows.forEach(function (row) {
expectRow(row, [
expectExtension,
expectAvgBytes,
expectCountry,
expectAvgBytes,
expectOS,
expectAvgBytes
]);
});
});
});

View file

@ -17,13 +17,9 @@
* under the License.
*/
import _ from 'lodash';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { TabbedAggResponseWriter } from '../_response_writer';
import { TabifyTableGroup } from '../_table_group';
import { TabifyBuckets } from '../_buckets';
import { VisProvider } from '../../../vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
@ -32,70 +28,55 @@ describe('TabbedAggResponseWriter class', function () {
let Private;
let indexPattern;
function defineSetup() {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
Private = $injector.get('Private');
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
Private = $injector.get('Private');
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));
}
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));
const splitAggConfig = [ {
type: 'terms',
params: {
field: 'geo.src',
}
}];
const twoSplitsAggConfig = [{
type: 'terms',
params: {
field: 'geo.src',
}
}, {
type: 'terms',
params: {
field: 'machine.os.raw',
}
}];
const createResponseWritter = (aggs = [], opts = {}) => {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: aggs });
return new TabbedAggResponseWriter(vis.getAggConfig(), opts);
};
describe('Constructor', function () {
defineSetup();
it('sets canSplit=true by default', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('canSplit', true);
let responseWriter;
beforeEach(() => {
responseWriter = createResponseWritter(twoSplitsAggConfig);
});
it('sets canSplit=false when config says to', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
canSplit: false,
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('canSplit', false);
it('creates aggStack', () => {
expect(responseWriter.aggStack.length).to.eql(3);
});
describe('sets partialRows', function () {
it('to the value of the config if set', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const partial = Boolean(Math.round(Math.random()));
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical(),
partialRows: partial
});
expect(writer).to.have.property('partialRows', partial);
});
it('to the value of vis.isHierarchical if no config', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const hierarchical = Boolean(Math.round(Math.random()));
sinon.stub(vis, 'isHierarchical').returns(hierarchical);
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('partialRows', hierarchical);
});
it('generates columns', () => {
expect(responseWriter.columns.length).to.eql(3);
});
it('starts off with a root TabifyTableGroup', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
expect(writer.root).to.be.a(TabifyTableGroup);
expect(writer.splitStack).to.be.an('array');
expect(writer.splitStack).to.have.length(1);
expect(writer.splitStack[0]).to.be(writer.root);
it('correctly generates columns with metricsAtAllLevels set to true', () => {
const minimalColumnsResponseWriter = createResponseWritter(twoSplitsAggConfig, { metricsAtAllLevels: true });
expect(minimalColumnsResponseWriter.columns.length).to.eql(4);
});
describe('sets timeRange', function () {
@ -128,296 +109,71 @@ describe('TabbedAggResponseWriter class', function () {
});
});
describe('', function () {
defineSetup();
describe('row()', function () {
let responseWriter;
describe('#response()', function () {
it('returns the root TabifyTableGroup if splitting', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
expect(writer.response()).to.be(writer.root);
});
it('returns the first table if not splitting', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical(),
canSplit: false
});
const table = writer._table();
expect(writer.response()).to.be(table);
});
it('adds columns to all of the tables', function () {
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{ type: 'terms', params: { field: '_type' }, schema: 'split' },
{ type: 'count', schema: 'metric' }
]
});
const buckets = new TabifyBuckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
const tables = [];
writer.split(vis.aggs[0], buckets, function () {
writer.cell(vis.aggs[1], 100, function () {
tables.push(writer.row());
});
});
tables.forEach(function (table) {
expect(table.columns == null).to.be(true);
});
const resp = writer.response();
expect(resp).to.be.a(TabifyTableGroup);
expect(resp.tables).to.have.length(2);
const nginx = resp.tables.shift();
expect(nginx).to.have.property('aggConfig', vis.aggs[0]);
expect(nginx).to.have.property('key', 'nginx');
expect(nginx.tables).to.have.length(1);
nginx.tables.forEach(function (table) {
expect(_.contains(tables, table)).to.be(true);
});
const apache = resp.tables.shift();
expect(apache).to.have.property('aggConfig', vis.aggs[0]);
expect(apache).to.have.property('key', 'apache');
expect(apache.tables).to.have.length(1);
apache.tables.forEach(function (table) {
expect(_.contains(tables, table)).to.be(true);
});
tables.forEach(function (table) {
expect(table.columns).to.be.an('array');
expect(table.columns).to.have.length(1);
expect(table.columns[0].aggConfig.type.name).to.be('count');
});
});
beforeEach(() => {
responseWriter = createResponseWritter(splitAggConfig, { partialRows: true });
});
describe('#split()', function () {
it('with break if the user has specified that splitting is to be disabled', function () {
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{ type: 'terms', schema: 'split', params: { field: '_type' } },
{ type: 'count', schema: 'metric' }
]
});
const agg = vis.aggs.bySchemaName.split[0];
const buckets = new TabifyBuckets({ buckets: [ { key: 'apache' } ] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical(),
canSplit: false
});
expect(function () {
writer.split(agg, buckets, _.noop);
}).to.throwException(/splitting is disabled/);
});
it('forks the acrStack and rewrites the parents', function () {
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{ type: 'terms', params: { field: 'extension' }, schema: 'segment' },
{ type: 'terms', params: { field: '_type' }, schema: 'split' },
{ type: 'terms', params: { field: 'machine.os' }, schema: 'segment' },
{ type: 'count', schema: 'metric' }
]
});
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical(),
asAggConfigResults: true
});
const extensions = new TabifyBuckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] });
const types = new TabifyBuckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const os = new TabifyBuckets({ buckets: [ { key: 'window' }, { key: 'osx' } ] });
extensions.forEach(function (b, extension) {
writer.cell(vis.aggs[0], extension, function () {
writer.split(vis.aggs[1], types, function () {
os.forEach(function (b, os) {
writer.cell(vis.aggs[2], os, function () {
writer.cell(vis.aggs[3], 200, function () {
writer.row();
});
});
});
});
});
});
const tables = _.flattenDeep(_.pluck(writer.response().tables, 'tables'));
expect(tables.length).to.be(types.length);
// collect the far left acr from each table
const leftAcrs = _.pluck(tables, 'rows[0][0]');
leftAcrs.forEach(function (acr, i, acrs) {
expect(acr.aggConfig).to.be(vis.aggs[0]);
expect(acr.$parent.aggConfig).to.be(vis.aggs[1]);
expect(acr.$parent.$parent).to.be(void 0);
// for all but the last acr, compare to the next
if (i + 1 >= acrs.length) return;
const acr2 = leftAcrs[i + 1];
expect(acr.key).to.be(acr2.key);
expect(acr.value).to.be(acr2.value);
expect(acr.aggConfig).to.be(acr2.aggConfig);
expect(acr.$parent).to.not.be(acr2.$parent);
});
});
it('adds the row to the array', () => {
responseWriter.rowBuffer['col-0'] = 'US';
responseWriter.rowBuffer['col-1'] = 5;
responseWriter.row();
expect(responseWriter.rows.length).to.eql(1);
expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 });
});
describe('#cell()', function () {
it('logs a cell in the TabbedAggResponseWriters row buffer, calls the block arg, then removes the value from the buffer',
function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
expect(writer.rowBuffer).to.have.length(0);
writer.cell({}, 500, function () {
expect(writer.rowBuffer).to.have.length(1);
expect(writer.rowBuffer[0]).to.be(500);
});
expect(writer.rowBuffer).to.have.length(0);
});
it('correctly handles bucketBuffer', () => {
responseWriter.bucketBuffer.push({ id: 'col-0', value: 'US' });
responseWriter.rowBuffer['col-1'] = 5;
responseWriter.row();
expect(responseWriter.rows.length).to.eql(1);
expect(responseWriter.rows[0]).to.eql({ 'col-0': 'US', 'col-1': 5 });
});
describe('#row()', function () {
it('writes the TabbedAggResponseWriters internal rowBuffer into a table', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
it('doesn\'t add an empty row', () => {
responseWriter.row();
expect(responseWriter.rows.length).to.eql(0);
});
});
const table = writer._table();
writer.cell({}, 1, function () {
writer.cell({}, 2, function () {
writer.cell({}, 3, function () {
writer.row();
});
});
});
describe('response()', () => {
let responseWriter;
expect(table.rows).to.have.length(1);
expect(table.rows[0]).to.eql([1, 2, 3]);
});
beforeEach(() => {
responseWriter = createResponseWritter(splitAggConfig);
});
it('always writes to the table group at the top of the split stack', function () {
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{ type: 'terms', schema: 'split', params: { field: '_type' } },
{ type: 'terms', schema: 'split', params: { field: 'extension' } },
{ type: 'terms', schema: 'split', params: { field: 'machine.os' } },
{ type: 'count', schema: 'metric' }
]
});
const splits = vis.aggs.bySchemaName.split;
it('produces correct response', () => {
responseWriter.rowBuffer['col-0-1'] = 'US';
responseWriter.rowBuffer['col-1-2'] = 5;
responseWriter.row();
const response = responseWriter.response();
expect(response).to.have.property('rows');
expect(response.rows).to.eql([{ 'col-0-1': 'US', 'col-1-2': 5 }]);
expect(response).to.have.property('columns');
expect(response.columns.length).to.equal(2);
expect(response.columns[0]).to.have.property('id', 'col-0-1');
expect(response.columns[0]).to.have.property('name', 'geo.src: Descending');
expect(response.columns[0]).to.have.property('aggConfig');
expect(response.columns[1]).to.have.property('id', 'col-1-2');
expect(response.columns[1]).to.have.property('name', 'Count');
expect(response.columns[1]).to.have.property('aggConfig');
});
const type = splits[0];
const typeTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const ext = splits[1];
const extTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] });
const os = splits[2];
const osTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'windows' }, { key: 'mac' } ] });
const count = vis.aggs[3];
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
writer.split(type, typeTabifyBuckets, function () {
writer.split(ext, extTabifyBuckets, function () {
writer.split(os, osTabifyBuckets, function (bucket, key) {
writer.cell(count, key === 'windows' ? 1 : 2, function () {
writer.row();
});
});
});
});
const resp = writer.response();
let sum = 0;
let tables = 0;
(function recurse(t) {
if (t.tables) {
// table group
t.tables.forEach(function (tt) {
recurse(tt);
});
} else {
tables += 1;
// table
t.rows.forEach(function (row) {
row.forEach(function (cell) {
sum += cell;
});
});
}
}(resp));
expect(tables).to.be(8);
expect(sum).to.be(12);
});
it('writes partial rows for hierarchical vis', function () {
const vis = new Vis(indexPattern, {
type: 'pie',
aggs: [
{ type: 'terms', schema: 'segment', params: { field: '_type' } },
{ type: 'count', schema: 'metric' }
]
});
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
const table = writer._table();
writer.cell(vis.aggs[0], 'apache', function () {
writer.row();
});
expect(table.rows).to.have.length(1);
expect(table.rows[0]).to.eql(['apache', '']);
});
it('skips partial rows for non-hierarchical vis', function () {
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: [
{ type: 'terms', schema: 'segment', params: { field: '_type' } },
{ type: 'count', schema: 'metric' }
]
});
const writer = new TabbedAggResponseWriter(vis.getAggConfig(), {
isHierarchical: vis.isHierarchical()
});
const table = writer._table();
writer.cell(vis.aggs[0], 'apache', function () {
writer.row();
});
expect(table.rows).to.have.length(0);
});
it('produces correct response for no data', () => {
const response = responseWriter.response();
expect(response).to.have.property('rows');
expect(response.rows.length).to.be(0);
expect(response).to.have.property('columns');
expect(response.columns.length).to.equal(2);
expect(response.columns[0]).to.have.property('id', 'col-0-1');
expect(response.columns[0]).to.have.property('name', 'geo.src: Descending');
expect(response.columns[0]).to.have.property('aggConfig');
expect(response.columns[1]).to.have.property('id', 'col-1-2');
expect(response.columns[1]).to.have.property('name', 'Count');
expect(response.columns[1]).to.have.property('aggConfig');
});
});
});

View file

@ -1,94 +0,0 @@
/*
* 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 expect from 'expect.js';
import { TabifyTable } from '../_table';
describe('TabifyTable class', function () {
it('exposes rows array, but not the columns', function () {
const table = new TabifyTable();
expect(table.rows).to.be.an('array');
expect(table.columns == null).to.be.ok();
});
describe('#aggConfig', function () {
it('accepts a column from the table and returns its agg config', function () {
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: football
};
expect(table.aggConfig(column)).to.be(football);
});
it('throws a TypeError if the column is malformed', function () {
expect(function () {
const notAColumn = {};
(new TabifyTable()).aggConfig(notAColumn);
}).to.throwException(TypeError);
});
});
describe('#title', function () {
it('returns nothing if the table is not part of a table group', function () {
const table = new TabifyTable();
expect(table.title()).to.be('');
});
it('returns the title of the TabifyTableGroup if the table is part of one', function () {
const table = new TabifyTable();
table.$parent = {
title: 'TabifyTableGroup Title',
tables: [table]
};
expect(table.title()).to.be('TabifyTableGroup Title');
});
});
describe('#field', function () {
it('calls the columns aggConfig#getField() method', function () {
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: {
getField: _.constant(football)
}
};
expect(table.field(column)).to.be(football);
});
});
describe('#fieldFormatter', function () {
it('calls the columns aggConfig#fieldFormatter() method', function () {
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: {
fieldFormatter: _.constant(football)
}
};
expect(table.fieldFormatter(column)).to.be(football);
});
});
});

View file

@ -1,32 +0,0 @@
/*
* 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 expect from 'expect.js';
import { TabifyTableGroup } from '../_table_group';
describe('Table Group class', function () {
it('exposes tables array and empty aggConfig, key and title', function () {
const tableGroup = new TabifyTableGroup();
expect(tableGroup.tables).to.be.an('array');
expect(tableGroup.aggConfig).to.be(null);
expect(tableGroup.key).to.be(null);
expect(tableGroup.title).to.be(null);
});
});

View file

@ -19,8 +19,6 @@
import './_get_columns';
import './_buckets';
import './_table';
import './_table_group';
import './_response_writer';
import './_integration';
describe('Tabify Agg Response', function () {

View file

@ -19,15 +19,19 @@
import _ from 'lodash';
export function tabifyGetColumns(aggs, minimal, hierarchical) {
const getColumn = (agg, i) => {
return {
aggConfig: agg,
id: `col-${i}-${agg.id}`,
name: agg.makeLabel()
};
};
if (minimal == null) minimal = !hierarchical;
export function tabifyGetColumns(aggs, minimal) {
// pick the columns
if (minimal) {
return aggs.map(function (agg) {
return { aggConfig: agg };
});
return aggs.map((agg, i) => getColumn(agg, i));
}
// supposed to be bucket,...metrics,bucket,...metrics
@ -40,16 +44,15 @@ export function tabifyGetColumns(aggs, minimal, hierarchical) {
if (!grouped.buckets) {
// return just the metrics, in column format
return grouped.metrics.map(function (agg) {
return { aggConfig: agg };
});
return grouped.metrics.map((agg, i) => getColumn(agg, i));
}
let columnIndex = 0;
// return the buckets, and after each place all of the metrics
grouped.buckets.forEach(function (agg) {
columns.push({ aggConfig: agg });
columns.push(getColumn(agg, columnIndex++));
grouped.metrics.forEach(function (metric) {
columns.push({ aggConfig: metric });
columns.push(getColumn(metric, columnIndex++));
});
});

View file

@ -17,294 +17,74 @@
* under the License.
*/
import _ from 'lodash';
import AggConfigResult from '../../vis/agg_config_result';
import { TabifyTable } from './_table';
import { TabifyTableGroup } from './_table_group';
import { toArray } from 'lodash';
import { tabifyGetColumns } from './_get_columns';
import { createLegacyClass } from '../../utils/legacy_class';
createLegacyClass(SplitAcr).inherits(AggConfigResult);
function SplitAcr(agg, parent, key) {
SplitAcr.Super.call(this, agg, parent, key, key);
}
/**
* Writer class that collects information about an aggregation response and
* produces a table, or a series of tables.
*
* @param {Vis} vis - the vis object to which the aggregation response correlates
* @param {AggConfigs} aggs - the agg configs object to which the aggregation response correlates
* @param {boolean} metricsAtAllLevels - setting to true will produce metrics for every bucket
* @param {boolean} partialRows - setting to true will not remove rows with missing values
*/
function TabbedAggResponseWriter(aggs, opts) {
this.opts = opts || {};
this.rowBuffer = [];
const visIsHier = opts.isHierarchical;
// do the options allow for splitting? we will only split if true and
// tabify calls the split method.
this.canSplit = this.opts.canSplit !== false;
// should we allow partial rows to be included in the tables? if a
// partial row is found, it is filled with empty strings ''
this.partialRows = this.opts.partialRows == null ? visIsHier : this.opts.partialRows;
// if true, we will not place metric columns after every bucket
// even if the vis is hierarchical. if false, and the vis is
// hierarchical, then we will display metric columns after
// every bucket col
this.minimalColumns = visIsHier ? !!this.opts.minimalColumns : true;
// true if we can expect metrics to have been calculated
// for every bucket
this.metricsForAllBuckets = visIsHier;
// if true, values will be wrapped in aggConfigResult objects which link them
// to their aggConfig and enable the filterbar and tooltip formatters
this.asAggConfigResults = !!this.opts.asAggConfigResults;
function TabbedAggResponseWriter(aggs, { metricsAtAllLevels = false, partialRows = false, timeRange } = {}) {
this.rowBuffer = {};
this.bucketBuffer = [];
this.metricBuffer = [];
this.metricsForAllBuckets = metricsAtAllLevels;
this.partialRows = partialRows;
this.aggs = aggs;
this.columns = tabifyGetColumns(aggs.getResponseAggs(), this.minimalColumns);
this.aggStack = _.pluck(this.columns, 'aggConfig');
this.columns = tabifyGetColumns(aggs.getResponseAggs(), !metricsAtAllLevels);
this.aggStack = [...this.columns];
this.root = new TabifyTableGroup();
this.acrStack = [];
this.splitStack = [this.root];
this.rows = [];
// Extract the time range object if provided
if (this.opts.timeRange) {
const timeRangeKey = Object.keys(this.opts.timeRange)[0];
this.timeRange = this.opts.timeRange[timeRangeKey];
if (timeRange) {
const timeRangeKey = Object.keys(timeRange)[0];
this.timeRange = timeRange[timeRangeKey];
if (this.timeRange) {
this.timeRange.name = timeRangeKey;
}
}
}
/**
* Create a Table of TableGroup object, link it to it's parent (if any), and determine if
* it's the root
*
* @param {boolean} group - is this a TableGroup or just a normal Table
* @param {AggConfig} agg - the aggregation that create this table, only applies to groups
* @param {any} key - the bucketKey that this table relates to
* @return {Table/TableGroup} table - the created table
*/
TabbedAggResponseWriter.prototype._table = function (group, agg, key) {
const Class = (group) ? TabifyTableGroup : TabifyTable;
const table = new Class();
const parent = this.splitStack[0];
if (group) {
table.aggConfig = agg;
table.key = key;
table.title = (table.fieldFormatter()(key));
// aggs that don't implement makeLabel should not add to title
if (agg.makeLabel() !== agg.name) {
table.title += ': ' + agg.makeLabel();
}
}
// link the parent and child
table.$parent = parent;
parent.tables.push(table);
return table;
TabbedAggResponseWriter.prototype.isPartialRow = function (row) {
return !this.columns.map(column => row.hasOwnProperty(column.id)).every(c => (c === true));
};
/**
* Enter into a split table, called for each bucket of a splitting agg. The new table
* is either created or located using the agg and key arguments, and then the block is
* executed with the table as it's this context. Within this function, you should
* walk into the remaining branches and end up writing some rows to the table.
*
* @param {aggConfig} agg - the aggConfig that created this split
* @param {Buckets} buckets - the buckets produces by the agg
* @param {function} block - a function to execute for each sub bucket
* Create a new row by reading the row buffer and bucketBuffer
*/
TabbedAggResponseWriter.prototype.split = function (agg, buckets, block) {
const self = this;
if (!self.canSplit) {
throw new Error('attempted to split when splitting is disabled');
}
self._removeAggFromColumns(agg);
buckets.forEach(function (bucket, key) {
// find the existing split that we should extend
let tableGroup = _.find(self.splitStack[0].tables, { aggConfig: agg, key: key });
// create the split if it doesn't exist yet
if (!tableGroup) tableGroup = self._table(true, agg, key);
let splitAcr = false;
if (self.asAggConfigResults) {
splitAcr = self._injectParentSplit(agg, key);
}
// push the split onto the stack so that it will receive written tables
self.splitStack.unshift(tableGroup);
// call the block
if (_.isFunction(block)) block.call(self, bucket, key);
// remove the split from the stack
self.splitStack.shift();
splitAcr && _.pull(self.acrStack, splitAcr);
});
};
TabbedAggResponseWriter.prototype._removeAggFromColumns = function (agg) {
const i = _.findIndex(this.columns, function (col) {
return col.aggConfig === agg;
TabbedAggResponseWriter.prototype.row = function () {
this.bucketBuffer.forEach(bucket => {
this.rowBuffer[bucket.id] = bucket.value;
});
// we must have already removed this column
if (i === -1) return;
this.columns.splice(i, 1);
if (this.minimalColumns) return;
// hierarchical vis creates additional columns for each bucket
// we will remove those too
const mCol = this.columns.splice(i, 1).pop();
const mI = _.findIndex(this.aggStack, function (agg) {
return agg === mCol.aggConfig;
this.metricBuffer.forEach(metric => {
this.rowBuffer[metric.id] = metric.value;
});
if (mI > -1) this.aggStack.splice(mI, 1);
};
/**
* When a split is found while building the aggConfigResult tree, we
* want to push the split into the tree at another point. Since each
* branch in the tree is a double-linked list we need do some special
* shit to pull this off.
*
* @private
* @param {AggConfig} - The agg which produced the split bucket
* @param {any} - The value which identifies the bucket
* @return {SplitAcr} - the AggConfigResult created for the split bucket
*/
TabbedAggResponseWriter.prototype._injectParentSplit = function (agg, key) {
const oldList = this.acrStack;
const newList = this.acrStack = [];
// walk from right to left through the old stack
// and move things to the new stack
let injected = false;
if (!oldList.length) {
injected = new SplitAcr(agg, null, key);
newList.unshift(injected);
return injected;
}
// walk from right to left, emptying the previous list
while (oldList.length) {
const acr = oldList.pop();
// ignore other splits
if (acr instanceof SplitAcr) {
newList.unshift(acr);
continue;
}
// inject the split
if (!injected) {
injected = new SplitAcr(agg, newList[0], key);
newList.unshift(injected);
}
const newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr), acr.filters);
newList.unshift(newAcr);
// and replace the acr in the row buffer if its there
const rowI = this.rowBuffer.indexOf(acr);
if (rowI > -1) {
this.rowBuffer[rowI] = newAcr;
}
}
return injected;
};
/**
* Push a value into the row, then run a block. Once the block is
* complete the value is pulled from the stack.
*
* @param {any} value - the value that should be added to the row
* @param {function} block - the function to run while this value is in the row
* @return {any} - the value that was added
*/
TabbedAggResponseWriter.prototype.cell = function (agg, value, block, filters) {
if (this.asAggConfigResults) {
value = new AggConfigResult(agg, this.acrStack[0], value, value, filters);
}
const stackResult = this.asAggConfigResults && value.type === 'bucket';
this.rowBuffer.push(value);
if (stackResult) this.acrStack.unshift(value);
if (_.isFunction(block)) block.call(this);
this.rowBuffer.pop(value);
if (stackResult) this.acrStack.shift();
return value;
};
/**
* Create a new row by reading the row buffer. This will do nothing if
* the row is incomplete and the vis this data came from is NOT flagged as
* hierarchical.
*
* @param {array} [buffer] - optional buffer to use in place of the stored rowBuffer
* @return {undefined}
*/
TabbedAggResponseWriter.prototype.row = function (buffer) {
const cells = buffer || this.rowBuffer.slice(0);
if (!this.partialRows && cells.length < this.columns.length) {
if (!toArray(this.rowBuffer).length || (!this.partialRows && this.isPartialRow(this.rowBuffer))) {
return;
}
const split = this.splitStack[0];
const table = split.tables[0] || this._table(false);
while (cells.length < this.columns.length) cells.push('');
table.rows.push(cells);
return table;
this.rows.push(this.rowBuffer);
this.rowBuffer = {};
};
/**
* Get the actual response
*
* @return {object} - the final table-tree
* @return {object} - the final table
*/
TabbedAggResponseWriter.prototype.response = function () {
const columns = this.columns;
// give the columns some metadata
columns.map(function (col) {
col.title = col.aggConfig.makeLabel();
});
// walk the tree and write the columns to each table
((function step(table) {
if (table.tables) table.tables.forEach(step);
else table.columns = columns.slice(0);
})(this.root));
if (this.canSplit) return this.root;
const table = this.root.tables[0];
if (!table) return;
delete table.$parent;
return table;
return {
columns: this.columns,
rows: this.rows
};
};
export { TabbedAggResponseWriter };

View file

@ -1,53 +0,0 @@
/*
* 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.
*/
/**
* Simple table class that is used to contain the rows and columns that create
* a table. This is usually found at the root of the response or within a TableGroup
*/
function TabifyTable() {
this.columns = null; // written with the first row
this.rows = [];
}
TabifyTable.prototype.title = function () {
if (this.$parent) {
return this.$parent.title;
} else {
return '';
}
};
TabifyTable.prototype.aggConfig = function (col) {
if (!col.aggConfig) {
throw new TypeError('Column is missing the aggConfig property');
}
return col.aggConfig;
};
TabifyTable.prototype.field = function (col) {
return this.aggConfig(col).getField();
};
TabifyTable.prototype.fieldFormatter = function (col) {
return this.aggConfig(col).fieldFormatter();
};
export { TabifyTable };

View file

@ -1,39 +0,0 @@
/*
* 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.
*/
/**
* Simple object that wraps multiple tables. It contains information about the aggConfig
* and bucket that created this group and a list of the tables within it.
*/
function TabifyTableGroup() {
this.aggConfig = null;
this.key = null;
this.title = null;
this.tables = [];
}
TabifyTableGroup.prototype.field = function () {
if (this.aggConfig) return this.aggConfig.getField();
};
TabifyTableGroup.prototype.fieldFormatter = function () {
if (this.aggConfig) return this.aggConfig.fieldFormatter();
};
export { TabifyTableGroup };

View file

@ -43,7 +43,8 @@ export function tabifyAggResponse(aggs, esResponse, respOpts = {}) {
* @returns {undefined}
*/
function collectBucket(write, bucket, key, aggScale) {
const agg = write.aggStack.shift();
const column = write.aggStack.shift();
const agg = column.aggConfig;
const aggInfo = agg.write(write.aggs);
aggScale *= aggInfo.metricScale || 1;
@ -51,23 +52,24 @@ function collectBucket(write, bucket, key, aggScale) {
case 'buckets':
const buckets = new TabifyBuckets(bucket[agg.id], agg.params, write.timeRange);
if (buckets.length) {
const splitting = write.canSplit && agg.schema.name === 'split';
if (splitting) {
write.split(agg, buckets, function forEachBucket(subBucket, key) {
collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale);
});
} else {
buckets.forEach(function (subBucket, key) {
write.cell(agg, agg.getKey(subBucket, key), function () {
collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale);
}, subBucket.filters);
});
}
} else if (write.partialRows && write.metricsForAllBuckets && write.minimalColumns) {
buckets.forEach(function (subBucket, key) {
// if the bucket doesn't have value don't add it to the row
// we don't want rows like: { column1: undefined, column2: 10 }
const bucketValue = agg.getKey(subBucket, key);
const hasBucketValue = typeof bucketValue !== 'undefined';
if (hasBucketValue) {
write.bucketBuffer.push({ id: column.id, value: bucketValue });
}
collectBucket(write, subBucket, agg.getKey(subBucket, key), aggScale);
if (hasBucketValue) {
write.bucketBuffer.pop();
}
});
} else if (write.partialRows) {
// we don't have any buckets, but we do have metrics at this
// level, then pass all the empty buckets and jump back in for
// the metrics.
write.aggStack.unshift(agg);
write.aggStack.unshift(column);
passEmptyBuckets(write, bucket, key, aggScale);
write.aggStack.shift();
} else {
@ -83,39 +85,41 @@ function collectBucket(write, bucket, key, aggScale) {
if (aggScale !== 1) {
value *= aggScale;
}
write.cell(agg, value, function () {
if (!write.aggStack.length) {
// row complete
write.row();
} else {
// process the next agg at this same level
collectBucket(write, bucket, key, aggScale);
}
});
write.metricBuffer.push({ id: column.id, value: value });
if (!write.aggStack.length) {
// row complete
write.row();
} else {
// process the next agg at this same level
collectBucket(write, bucket, key, aggScale);
}
write.metricBuffer.pop();
break;
}
write.aggStack.unshift(agg);
write.aggStack.unshift(column);
}
// write empty values for each bucket agg, then write
// the metrics from the initial bucket using collectBucket()
function passEmptyBuckets(write, bucket, key, aggScale) {
const agg = write.aggStack.shift();
const column = write.aggStack.shift();
const agg = column.aggConfig;
switch (agg.type.type) {
case 'metrics':
// pass control back to collectBucket()
write.aggStack.unshift(agg);
write.aggStack.unshift(column);
collectBucket(write, bucket, key, aggScale);
return;
case 'buckets':
write.cell(agg, '', function () {
passEmptyBuckets(write, bucket, key, aggScale);
});
passEmptyBuckets(write, bucket, key, aggScale);
}
write.aggStack.unshift(agg);
write.aggStack.unshift(column);
}

View file

@ -21,7 +21,7 @@ import $ from 'jquery';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import fixtures from 'fixtures/fake_hierarchical_data';
import { tabifyAggResponse } from '../../agg_response/tabify/tabify';
import { LegacyResponseHandlerProvider } from '../../vis/response_handlers/legacy';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from '../../vis';
describe('AggTableGroup Directive', function () {
@ -30,9 +30,11 @@ describe('AggTableGroup Directive', function () {
let $compile;
let Vis;
let indexPattern;
let tableAggResponse;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private) {
tableAggResponse = Private(LegacyResponseHandlerProvider).handler;
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
Vis = Private(VisProvider);
@ -49,9 +51,9 @@ describe('AggTableGroup Directive', function () {
});
it('renders a simple split response properly', function () {
it('renders a simple split response properly', async function () {
const vis = new Vis(indexPattern, 'table');
$scope.group = tabifyAggResponse(vis.getAggConfig(), fixtures.metricOnly);
$scope.group = await tableAggResponse(vis, fixtures.metricOnly);
$scope.sort = {
columnIndex: null,
direction: null
@ -79,7 +81,7 @@ describe('AggTableGroup Directive', function () {
expect($subTables.length).to.be(0);
});
it('renders a complex response properly', function () {
it('renders a complex response properly', async function () {
const vis = new Vis(indexPattern, {
type: 'pie',
aggs: [
@ -93,7 +95,7 @@ describe('AggTableGroup Directive', function () {
agg.id = 'agg_' + (i + 1);
});
const group = $scope.group = tabifyAggResponse(vis.getAggConfig(), fixtures.threeTermBuckets);
const group = $scope.group = await tableAggResponse(vis, fixtures.threeTermBuckets);
const $el = $('<kbn-agg-table-group group="group"></kbn-agg-table-group>');
$compile($el)($scope);
$scope.$digest();

View file

@ -17,14 +17,13 @@
* under the License.
*/
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';
import sinon from 'sinon';
import { tabifyAggResponse } from '../../agg_response/tabify/tabify';
import { LegacyResponseHandlerProvider } from '../../vis/response_handlers/legacy';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from '../../vis';
describe('AggTable Directive', function () {
@ -34,9 +33,11 @@ describe('AggTable Directive', function () {
let Vis;
let indexPattern;
let settings;
let tableAggResponse;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private, config) {
tableAggResponse = Private(LegacyResponseHandlerProvider).handler;
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
Vis = Private(VisProvider);
settings = config;
@ -54,20 +55,19 @@ describe('AggTable Directive', function () {
});
it('renders a simple response properly', function () {
it('renders a simple response properly', async function () {
const vis = new Vis(indexPattern, 'table');
$scope.table = tabifyAggResponse(
vis.getAggConfig(),
fixtures.metricOnly,
{ canSplit: false, hierarchical: vis.isHierarchical() }
);
$scope.table = (await tableAggResponse(
vis,
fixtures.metricOnly
)).tables[0];
const $el = $compile('<kbn-agg-table table="table"></kbn-agg-table>')($scope);
$scope.$digest();
expect($el.find('tbody').length).to.be(1);
expect($el.find('td').length).to.be(1);
expect($el.find('td').text()).to.eql(1000);
expect($el.find('td').text()).to.eql('1,000');
});
it('renders nothing if the table is empty', function () {
@ -78,24 +78,24 @@ describe('AggTable Directive', function () {
expect($el.find('tbody').length).to.be(0);
});
it('renders a complex response properly', function () {
it('renders a complex response properly', async function () {
const vis = new Vis(indexPattern, {
type: 'pie',
type: 'table',
params: {
showMetricsAtAllLevels: true
},
aggs: [
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } },
{ type: 'terms', schema: 'split', params: { field: 'extension' } },
{ type: 'terms', schema: 'segment', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'segment', params: { field: 'machine.os' } }
{ type: 'terms', schema: 'bucket', params: { field: 'extension' } },
{ type: 'terms', schema: 'bucket', params: { field: 'geo.src' } },
{ type: 'terms', schema: 'bucket', params: { field: 'machine.os' } }
]
});
vis.aggs.forEach(function (agg, i) {
agg.id = 'agg_' + (i + 1);
});
$scope.table = tabifyAggResponse(vis.getAggConfig(), fixtures.threeTermBuckets, {
canSplit: false,
isHierarchical: vis.isHierarchical()
});
$scope.table = (await tableAggResponse(vis, fixtures.threeTermBuckets)).tables[0];
const $el = $('<kbn-agg-table table="table"></kbn-agg-table>');
$compile($el)($scope);
$scope.$digest();
@ -106,9 +106,10 @@ describe('AggTable Directive', function () {
expect($rows.length).to.be.greaterThan(0);
function validBytes(str) {
expect(str).to.match(/^\d+$/);
const bytesAsNum = _.parseInt(str);
expect(bytesAsNum === 0 || bytesAsNum > 1000).to.be.ok();
const num = str.replace(/,/g, '');
if (num !== '-') {
expect(num).to.match(/^\d+$/);
}
}
$rows.each(function () {
@ -135,7 +136,7 @@ describe('AggTable Directive', function () {
});
describe('renders totals row', function () {
function totalsRowTest(totalFunc, expected) {
async function totalsRowTest(totalFunc, expected) {
const vis = new Vis(indexPattern, {
type: 'table',
aggs: [
@ -158,10 +159,10 @@ describe('AggTable Directive', function () {
const oldTimezoneSetting = settings.get('dateFormat:tz');
settings.set('dateFormat:tz', 'UTC');
$scope.table = tabifyAggResponse(vis.getAggConfig(),
$scope.table = (await tableAggResponse(vis,
fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative,
{ canSplit: false, minimalColumns: true, asAggConfigResults: true }
);
)).tables[0];
$scope.showTotal = true;
$scope.totalFunc = totalFunc;
const $el = $('<kbn-agg-table table="table" show-total="showTotal" total-func="totalFunc"></kbn-agg-table>');
@ -182,11 +183,11 @@ describe('AggTable Directive', function () {
settings.set('dateFormat:tz', oldTimezoneSetting);
off();
}
it('as count', function () {
totalsRowTest('count', ['18', '18', '18', '18', '18', '18']);
it('as count', async function () {
await totalsRowTest('count', ['18', '18', '18', '18', '18', '18']);
});
it('as min', function () {
totalsRowTest('min', [
it('as min', async function () {
await totalsRowTest('min', [
'',
'2014-09-28',
'9,283',
@ -195,8 +196,8 @@ describe('AggTable Directive', function () {
'11'
]);
});
it('as max', function () {
totalsRowTest('max', [
it('as max', async function () {
await totalsRowTest('max', [
'',
'2014-10-03',
'220,943',
@ -205,8 +206,8 @@ describe('AggTable Directive', function () {
'837'
]);
});
it('as avg', function () {
totalsRowTest('avg', [
it('as avg', async function () {
await totalsRowTest('avg', [
'',
'',
'87,221.5',
@ -215,8 +216,8 @@ describe('AggTable Directive', function () {
'206.833'
]);
});
it('as sum', function () {
totalsRowTest('sum', [
it('as sum', async function () {
await totalsRowTest('sum', [
'',
'',
'1,569,987',

View file

@ -102,10 +102,10 @@ uiModules
return;
}
self.csv.filename = ($scope.exportTitle || table.title() || 'table') + '.csv';
self.csv.filename = ($scope.exportTitle || table.title || 'table') + '.csv';
$scope.rows = table.rows;
$scope.formattedColumns = table.columns.map(function (col, i) {
const agg = $scope.table.aggConfig(col);
const agg = col.aggConfig;
const field = agg.getField();
const formattedColumn = {
title: col.title,

View file

@ -35,8 +35,7 @@ describe('AggTypeMetricMedianProvider class', function () {
'title': 'New Visualization',
'type': 'metric',
'params': {
'fontSize': 60,
'handleNoResults': true
'fontSize': 60
},
'aggs': [
{

View file

@ -59,8 +59,7 @@ describe('parent pipeline aggs', function () {
title: 'New Visualization',
type: 'metric',
params: {
fontSize: 60,
handleNoResults: true
fontSize: 60
},
aggs: [
{

View file

@ -68,8 +68,7 @@ describe('sibling pipeline aggs', function () {
title: 'New Visualization',
type: 'metric',
params: {
fontSize: 60,
handleNoResults: true
fontSize: 60
},
aggs: [
{

View file

@ -49,8 +49,7 @@ describe('Top hit metric', function () {
title: 'New Visualization',
type: 'metric',
params: {
fontSize: 60,
handleNoResults: true
fontSize: 60
},
aggs: [
{

View file

@ -269,10 +269,11 @@ describe('Vis Class', function () {
data = {
columns: [{
id: 'col-0',
title: 'test',
aggConfig
}],
rows: [['US']]
rows: [{ 'col-0': 'US' }]
};
});

View file

@ -21,9 +21,8 @@ import _ from 'lodash';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import { TabifyTable } from '../../../agg_response/tabify/_table';
import { AggResponseIndexProvider } from '../../../agg_response';
import { BasicResponseHandlerProvider } from '../../response_handlers/basic';
import { VislibResponseHandlerProvider } from '../../response_handlers/vislib';
describe('renderbot#buildChartData', function () {
let buildChartData;
@ -32,7 +31,7 @@ describe('renderbot#buildChartData', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
aggResponse = Private(AggResponseIndexProvider);
buildChartData = Private(BasicResponseHandlerProvider).handler;
buildChartData = Private(VislibResponseHandlerProvider).handler;
}));
describe('for hierarchical vis', function () {
@ -79,7 +78,7 @@ describe('renderbot#buildChartData', function () {
}
};
const esResp = { hits: { total: 1 } };
const tabbed = { tables: [ new TabifyTable() ] };
const tabbed = { tables: [ {}] };
sinon.stub(aggResponse, 'tabify').returns(tabbed);
expect(buildChartData.call(renderbot, esResp)).to.eql(chart);
@ -88,7 +87,7 @@ describe('renderbot#buildChartData', function () {
it('converts table groups into rows/columns wrappers for charts', function () {
const converter = sinon.stub().returns('chart');
const esResp = { hits: { total: 1 } };
const tables = [new TabifyTable(), new TabifyTable(), new TabifyTable(), new TabifyTable()];
const tables = [{}, {}, {}, {}];
const renderbot = {
vis: {

View file

@ -19,7 +19,7 @@
import ngMock from 'ng_mock';
import expect from 'expect.js';
import { BasicResponseHandlerProvider } from '../../response_handlers/basic';
import { VislibResponseHandlerProvider } from '../../response_handlers/vislib';
import { VisProvider } from '../..';
import fixtures from 'fixtures/fake_hierarchical_data';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
@ -39,7 +39,7 @@ describe('Basic Response Handler', function () {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
basicResponseHandler = Private(BasicResponseHandlerProvider).handler;
basicResponseHandler = Private(VislibResponseHandlerProvider).handler;
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));

View file

@ -39,9 +39,9 @@ describe('Vislib Vis Type', function () {
}));
describe('initialization', () => {
it('should set the basic response handler if not set', () => {
it('should set the vislib response handler if not set', () => {
const visType = new VislibVisType(visConfig);
expect(visType.responseHandler).to.equal('basic');
expect(visType.responseHandler).to.equal('vislib');
});
it('should not change response handler if its already set', () => {

View file

@ -28,27 +28,28 @@ export function convertToGeoJson(tabifiedResponse) {
let max = -Infinity;
let geoAgg;
if (tabifiedResponse && tabifiedResponse.tables && tabifiedResponse.tables[0] && tabifiedResponse.tables[0].rows) {
if (tabifiedResponse && tabifiedResponse.rows) {
const table = tabifiedResponse.tables[0];
const geohashIndex = table.columns.findIndex(column => column.aggConfig.type.dslName === 'geohash_grid');
geoAgg = table.columns.find(column => column.aggConfig.type.dslName === 'geohash_grid');
const table = tabifiedResponse;
const geohashColumn = table.columns.find(column => column.aggConfig.type.dslName === 'geohash_grid');
if (geohashIndex === -1) {
if (!geohashColumn) {
features = [];
} else {
const metricIndex = table.columns.findIndex(column => column.aggConfig.type.type === 'metrics');
const geocentroidIndex = table.columns.findIndex(column => column.aggConfig.type.dslName === 'geo_centroid');
geoAgg = geohashColumn.aggConfig;
const metricColumn = table.columns.find(column => column.aggConfig.type.type === 'metrics');
const geocentroidColumn = table.columns.find(column => column.aggConfig.type.dslName === 'geo_centroid');
features = table.rows.map(row => {
const geohash = row[geohashIndex];
const geohash = row[geohashColumn.id];
const geohashLocation = decodeGeoHash(geohash);
let pointCoordinates;
if (geocentroidIndex > -1) {
const location = row[geocentroidIndex];
if (geocentroidColumn) {
const location = row[geocentroidColumn.id];
pointCoordinates = [location.lon, location.lat];
} else {
pointCoordinates = [geohashLocation.longitude[2], geohashLocation.latitude[2]];
@ -66,13 +67,13 @@ export function convertToGeoJson(tabifiedResponse) {
geohashLocation.longitude[2]
];
if (geoAgg.aggConfig.params.useGeocentroid) {
if (geoAgg.params.useGeocentroid) {
// see https://github.com/elastic/elasticsearch/issues/24694 for why clampGrid is used
pointCoordinates[0] = clampGrid(pointCoordinates[0], geohashLocation.longitude[0], geohashLocation.longitude[1]);
pointCoordinates[1] = clampGrid(pointCoordinates[1], geohashLocation.latitude[0], geohashLocation.latitude[1]);
}
const value = row[metricIndex];
const value = row[metricColumn.id];
min = Math.min(min, value);
max = Math.max(max, value);
@ -111,8 +112,8 @@ export function convertToGeoJson(tabifiedResponse) {
meta: {
min: min,
max: max,
geohashPrecision: geoAgg && geoAgg.aggConfig.params.precision,
geohashGridDimensionsAtEquator: geoAgg && gridDimensions(geoAgg.aggConfig.params.precision)
geohashPrecision: geoAgg && geoAgg.params.precision,
geohashGridDimensionsAtEquator: geoAgg && gridDimensions(geoAgg.params.precision)
}
};
}

View file

@ -35,10 +35,8 @@ const CourierRequestHandlerProvider = function () {
*/
async function buildTabularInspectorData(vis, searchSource, aggConfigs) {
const table = tabifyAggResponse(aggConfigs, searchSource.finalResponse, {
canSplit: false,
asAggConfigResults: false,
partialRows: true,
isHierarchical: vis.isHierarchical(),
metricsAtAllLevels: vis.isHierarchical(),
});
const columns = table.columns.map((col, index) => {
const field = col.aggConfig.getField();
@ -46,7 +44,7 @@ const CourierRequestHandlerProvider = function () {
col.aggConfig.isFilterable()
&& (!field || field.filterable);
return ({
name: col.title,
name: col.name,
field: `col${index}`,
filter: isCellContentFilterable && ((value) => {
const filter = col.aggConfig.createFilter(value.raw);
@ -61,9 +59,10 @@ const CourierRequestHandlerProvider = function () {
});
});
const rows = table.rows.map(row => {
return row.reduce((prev, cur, index) => {
const fieldFormatter = table.columns[index].aggConfig.fieldFormatter('text');
prev[`col${index}`] = new FormattedData(cur, fieldFormatter(cur));
return table.columns.reduce((prev, cur, index) => {
const value = row[cur.id];
const fieldFormatter = cur.aggConfig.fieldFormatter('text');
prev[`col${index}`] = new FormattedData(value, fieldFormatter(value));
return prev;
}, {});
});

View file

@ -0,0 +1,110 @@
/*
* 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 { tabifyAggResponse } from '../../agg_response/tabify';
import AggConfigResult from '../../vis/agg_config_result';
import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers';
const LegacyResponseHandlerProvider = function () {
return {
name: 'legacy',
handler: function (vis, response) {
return new Promise((resolve) => {
const converted = { tables: [] };
const metricsAtAllLevels = vis.params.hasOwnProperty('showMetricsAtAllLevels') ?
vis.params.showMetricsAtAllLevels : vis.isHierarchical();
const table = tabifyAggResponse(vis.getAggConfig(), response, {
metricsAtAllLevels: metricsAtAllLevels,
partialRows: vis.params.showPartialRows,
});
const asAggConfigResults = _.get(vis, 'type.responseHandlerConfig.asAggConfigResults', false);
const splitColumn = table.columns.find(column => column.aggConfig.schema.name === 'split');
if (splitColumn) {
const splitAgg = splitColumn.aggConfig;
const splitMap = {};
let splitIndex = 0;
table.rows.forEach(row => {
const splitValue = row[splitColumn.id];
const splitColumnIndex = table.columns.findIndex(column => column === splitColumn);
if (!splitMap.hasOwnProperty(splitValue)) {
splitMap[splitValue] = splitIndex++;
const tableGroup = {
$parent: converted,
aggConfig: splitAgg,
title: `${splitValue}: ${splitAgg.makeLabel()}`,
key: splitValue,
tables: []
};
tableGroup.tables.push({
$parent: tableGroup,
columns: table.columns.filter((column, i) => i !== splitColumnIndex).map(column => ({ title: column.name, ...column })),
rows: []
});
converted.tables.push(tableGroup);
}
let previousSplitAgg = new AggConfigResult(splitAgg, null, splitValue, splitValue);
const tableIndex = splitMap[splitValue];
const newRow = _.map(converted.tables[tableIndex].tables[0].columns, column => {
const value = row[column.id];
const aggConfigResult = new AggConfigResult(column.aggConfig, previousSplitAgg, value, value);
if (column.aggConfig.type.type === 'buckets') {
previousSplitAgg = aggConfigResult;
}
return asAggConfigResults ? aggConfigResult : value;
});
converted.tables[tableIndex].tables[0].rows.push(newRow);
});
} else {
converted.tables.push({
columns: table.columns.map(column => ({ title: column.name, ...column })),
rows: table.rows.map(row => {
let previousSplitAgg;
return table.columns.map(column => {
const value = row[column.id];
const aggConfigResult = new AggConfigResult(column.aggConfig, previousSplitAgg, value, value);
if (column.aggConfig.type.type === 'buckets') {
previousSplitAgg = aggConfigResult;
}
return asAggConfigResults ? aggConfigResult : value;
});
}),
aggConfig: (column) => column.aggConfig
});
}
resolve(converted);
});
}
};
};
VisResponseHandlersRegistryProvider.register(LegacyResponseHandlerProvider);
export { LegacyResponseHandlerProvider };

View file

@ -17,7 +17,6 @@
* under the License.
*/
import _ from 'lodash';
import { AggResponseIndexProvider } from '../../agg_response';
import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers';
import { getTime } from 'ui/timefilter/get_time';
@ -32,10 +31,9 @@ const TabifyResponseHandlerProvider = function (Private) {
const time = getTime(vis.indexPattern, vis.filters.timeRange);
const tableGroup = aggResponse.tabify(vis.getAggConfig(), response, {
canSplit: true,
asAggConfigResults: _.get(vis, 'type.responseHandlerConfig.asAggConfigResults', false),
isHierarchical: vis.isHierarchical(),
timeRange: time ? time.range : undefined
metricsAtAllLevels: vis.isHierarchical(),
partialRows: vis.params.showPartialRows,
timeRange: time ? time.range : undefined,
});
resolve(tableGroup);

View file

@ -18,18 +18,18 @@
*/
import { AggResponseIndexProvider } from '../../agg_response';
import { TabifyTable } from '../../agg_response/tabify/_table';
import { getTime } from 'ui/timefilter/get_time';
import { LegacyResponseHandlerProvider } from './legacy';
import { VisResponseHandlersRegistryProvider } from '../../registry/vis_response_handlers';
const BasicResponseHandlerProvider = function (Private) {
const VislibResponseHandlerProvider = function (Private) {
const aggResponse = Private(AggResponseIndexProvider);
const tableResponseProvider = Private(LegacyResponseHandlerProvider).handler;
function convertTableGroup(vis, tableGroup) {
const tables = tableGroup.tables;
const firstChild = tables[0];
if (firstChild instanceof TabifyTable) {
if (firstChild.columns) {
const chart = convertTable(vis, firstChild);
// if chart is within a split, assign group title to its label
@ -40,6 +40,7 @@ const BasicResponseHandlerProvider = function (Private) {
}
if (!tables.length) return;
const out = {};
let outList;
@ -64,38 +65,34 @@ const BasicResponseHandlerProvider = function (Private) {
}
return {
name: 'basic',
name: 'vislib',
handler: function (vis, response) {
return new Promise((resolve) => {
if (vis.isHierarchical()) {
// the hierarchical converter is very self-contained (woot!)
// todo: it should be updated to be based on tabified data just as other responseConverters
resolve(aggResponse.hierarchical(vis, response));
}
const time = getTime(vis.indexPattern, vis.filters.timeRange);
return tableResponseProvider(vis, response).then(tableGroup => {
let converted = convertTableGroup(vis, tableGroup);
if (!converted) {
// mimic a row of tables that doesn't have any tables
// https://github.com/elastic/kibana/blob/7bfb68cd24ed42b1b257682f93c50cd8d73e2520/src/kibana/components/vislib/components/zero_injection/inject_zeros.js#L32
converted = { rows: [] };
}
const tableGroup = aggResponse.tabify(vis.getAggConfig(), response, {
canSplit: true,
asAggConfigResults: true,
isHierarchical: vis.isHierarchical(),
timeRange: time ? time.range : undefined
converted.hits = response.hits.total;
resolve(converted);
});
let converted = convertTableGroup(vis, tableGroup);
if (!converted) {
// mimic a row of tables that doesn't have any tables
// https://github.com/elastic/kibana/blob/7bfb68cd24ed42b1b257682f93c50cd8d73e2520/src/kibana/components/vislib/components/zero_injection/inject_zeros.js#L32
converted = { rows: [] };
}
converted.hits = response.hits.total;
resolve(converted);
});
}
};
};
VisResponseHandlersRegistryProvider.register(BasicResponseHandlerProvider);
VisResponseHandlersRegistryProvider.register(VislibResponseHandlerProvider);
export { BasicResponseHandlerProvider };
export { VislibResponseHandlerProvider };

View file

@ -49,7 +49,11 @@ const getTerms = (table, columnIndex, rowIndex) => {
}
// get only rows where cell value matches current row for all the fields before columnIndex
const rows = table.rows.filter(row => row.every((cell, i) => cell === table.rows[rowIndex][i] || i >= 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[columnIndex]);
return [...new Set(terms.filter(term => {
@ -99,17 +103,17 @@ export function VisProvider(Private, indexPatterns, getAppState) {
filterBarClickHandler(appState)(event);
},
addFilter: (data, columnIndex, rowIndex, cellValue) => {
const agg = data.columns[columnIndex].aggConfig;
const { aggConfig, id: columnId } = data.columns[columnIndex];
let filter = [];
const value = rowIndex > -1 ? data.rows[rowIndex][columnIndex] : cellValue;
const value = rowIndex > -1 ? data.rows[rowIndex][columnId] : cellValue;
if (!value) {
return;
}
if (agg.type.name === 'terms' && agg.params.otherBucket) {
if (aggConfig.type.name === 'terms' && aggConfig.params.otherBucket) {
const terms = getTerms(data, columnIndex, rowIndex);
filter = agg.createFilter(value, { terms });
filter = aggConfig.createFilter(value, { terms });
} else {
filter = agg.createFilter(value);
filter = aggConfig.createFilter(value);
}
queryFilter.addFilters(filter);
}, brush: (event) => {

View file

@ -108,7 +108,8 @@ export function VislibVisTypeProvider(Private, $rootScope, $timeout, $compile) {
class VislibVisType extends BaseVisType {
constructor(opts) {
if (!opts.responseHandler) {
opts.responseHandler = 'basic';
opts.responseHandler = 'vislib';
opts.responseHandlerConfig = { asAggConfigResults: true };
}
if (!opts.responseConverter) {
opts.responseConverter = pointSeries;

View file

@ -219,18 +219,7 @@ export function VisHandlerProvider(Private) {
// to continuously call render on resize
.attr('class', 'visualize-error chart error');
if (message === 'No results found') {
div.append('div')
.attr('class', 'text-center visualize-error visualize-chart')
.append('div').attr('class', 'item top')
.append('div').attr('class', 'item')
.append('h2').html('<i class="fa fa-meh-o"></i>')
.append('h4').text(message);
div.append('div').attr('class', 'item bottom');
} else {
div.append('h4').text(markdownIt.renderInline(message));
}
div.append('h4').text(markdownIt.renderInline(message));
dispatchRenderComplete(this.el);
return div;

View file

@ -43,7 +43,7 @@ class VisualizationStub {
describe('<Visualization/>', () => {
const visData = {
hits: { total: 1 }
hits: 1
};
const uiState = {
@ -63,19 +63,18 @@ describe('<Visualization/>', () => {
return this.uiState;
},
params: {
},
type: {
title: 'new vis',
requiresSearch: true,
handleNoResults: true,
useCustomNoDataScreen: false,
visualization: VisualizationStub
}
};
});
it('should display no result message when length of data is 0', () => {
const data = { hits: { total: 0 } };
const data = { rows: [] };
const wrapper = render(<Visualization vis={vis} visData={data} listenOnChange={true} uiState={uiState} />);
expect(wrapper.text()).toBe('No results found');
});
@ -87,7 +86,7 @@ describe('<Visualization/>', () => {
it('should call onInit when rendering no data', () => {
const spy = jest.fn();
const noData = { hits: { total: 0 } };
const noData = { hits: 0 };
mount(
<Visualization
vis={vis}

View file

@ -30,8 +30,9 @@ import './visualization.less';
function shouldShowNoResultsMessage(vis: Vis, visData: any): boolean {
const requiresSearch = get(vis, 'type.requiresSearch');
const isZeroHits = get(visData, 'hits.total') === 0;
const shouldShowMessage = !get(vis, 'params.handleNoResults');
const rows: object[] | undefined = get(visData, 'rows');
const isZeroHits = get(visData, 'hits') === 0 || (rows && !rows.length);
const shouldShowMessage = !get(vis, 'type.useCustomNoDataScreen');
return Boolean(requiresSearch && isZeroHits && shouldShowMessage);
}

View file

@ -18,15 +18,18 @@
*/
import React from 'react';
import { dispatchRenderComplete } from '../../render_complete';
interface VisualizationNoResultsProps {
onInit?: () => void;
}
export class VisualizationNoResults extends React.Component<VisualizationNoResultsProps> {
private containerDiv = React.createRef<HTMLDivElement>();
public render() {
return (
<div className="text-center visualize-error visualize-chart">
<div className="text-center visualize-error visualize-chart" ref={this.containerDiv}>
<div className="item top" />
<div className="item">
<h2 aria-hidden="true">
@ -40,14 +43,19 @@ export class VisualizationNoResults extends React.Component<VisualizationNoResul
}
public componentDidMount() {
if (this.props.onInit) {
this.props.onInit();
}
this.afterRender();
}
public componentDidUpdate() {
this.afterRender();
}
private afterRender() {
if (this.props.onInit) {
this.props.onInit();
}
if (this.containerDiv.current) {
dispatchRenderComplete(this.containerDiv.current);
}
}
}

View file

@ -46,9 +46,10 @@ export const UI_EXPORT_DEFAULTS = {
'ui/vis/request_handlers/none'
],
visResponseHandlers: [
'ui/vis/response_handlers/basic',
'ui/vis/response_handlers/vislib',
'ui/vis/response_handlers/none',
'ui/vis/response_handlers/tabify',
'ui/vis/response_handlers/legacy',
],
visEditorTypes: [
'ui/vis/editors/default/default',