removes angular and vis dependency from tabify (#17566) (#17673)

This commit is contained in:
Peter Pisljar 2018-04-12 19:53:48 -05:00 committed by GitHub
parent 3096c36117
commit 87260d266f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 681 additions and 755 deletions

View file

@ -1,10 +1,9 @@
// import 'ui/agg_table';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import tableSpyModeTemplate from 'plugins/spy_modes/table_spy_mode.html';
import { SpyModesRegistryProvider } from 'ui/registry/spy_modes';
function VisSpyTableProvider(Notifier, $filter, $rootScope, config, Private) {
const tabifyAggResponse = Private(AggResponseTabifyProvider);
function VisSpyTableProvider(Notifier, $filter, $rootScope) {
const PER_PAGE_DEFAULT = 10;
return {
@ -23,10 +22,11 @@ function VisSpyTableProvider(Notifier, $filter, $rootScope, config, Private) {
} else {
$scope.rowsPerPage = PER_PAGE_DEFAULT;
$scope.table = tabifyAggResponse($scope.vis, $scope.searchSource.rawResponse, {
$scope.table = tabifyAggResponse($scope.vis.getAggConfig().getResponseAggs(), $scope.searchSource.rawResponse, {
canSplit: false,
asAggConfigResults: true,
partialRows: true
partialRows: true,
isHierarchical: $scope.vis.isHierarchical()
});
}
});

View file

@ -2,8 +2,7 @@ import $ from 'jquery';
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import sinon from 'sinon';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { AppStateProvider } from 'ui/state_management/app_state';
@ -17,12 +16,10 @@ describe('Controller', function () {
let Vis;
let fixtures;
let AppState;
let tabify;
beforeEach(ngMock.module('kibana', 'kibana/table_vis'));
beforeEach(ngMock.inject(function ($injector) {
Private = $injector.get('Private');
tabify = Private(AggResponseTabifyProvider);
$rootScope = $injector.get('$rootScope');
$compile = $injector.get('$compile');
fixtures = require('fixtures/fake_hierarchical_data');
@ -90,7 +87,9 @@ describe('Controller', function () {
expect(!$scope.tableGroups).to.be.ok();
expect(!$scope.hasSomeRows).to.be.ok();
attachEsResponseToScope(tabify(vis, fixtures.oneRangeBucket));
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.oneRangeBucket, {
isHierarchical: vis.isHierarchical()
}));
expect($scope.hasSomeRows).to.be(true);
expect($scope.tableGroups).to.have.property('tables');
@ -103,7 +102,9 @@ describe('Controller', function () {
const vis = new OneRangeVis();
initController(vis);
attachEsResponseToScope(tabify(vis, fixtures.oneRangeBucket));
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.oneRangeBucket, {
isHierarchical: vis.isHierarchical()
}));
removeEsResponseFromScope();
expect(!$scope.hasSomeRows).to.be.ok();
@ -122,7 +123,9 @@ describe('Controller', function () {
const resp = _.cloneDeep(fixtures.oneRangeBucket);
resp.aggregations.agg_2.buckets = {};
attachEsResponseToScope(tabify(vis, resp));
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), resp, {
isHierarchical: vis.isHierarchical()
}));
expect($scope.sort.columnIndex).to.equal(sortObj.columnIndex);
expect($scope.sort.direction).to.equal(sortObj.direction);
@ -136,35 +139,27 @@ describe('Controller', function () {
const resp = _.cloneDeep(fixtures.oneRangeBucket);
resp.aggregations.agg_2.buckets = {};
attachEsResponseToScope(tabify(vis, resp));
attachEsResponseToScope(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), resp, {
isHierarchical: vis.isHierarchical()
}));
expect($scope.hasSomeRows).to.be(false);
expect(!$scope.tableGroups).to.be.ok();
});
it('passes partialRows:true to tabify based on the vis params', function () {
// spy on the tabify private module
const spiedTabify = sinon.spy(Private(AggResponseTabifyProvider));
Private.stub(AggResponseTabifyProvider, spiedTabify);
const vis = new OneRangeVis({ showPartialRows: true });
initController(vis);
attachEsResponseToScope(spiedTabify(vis, fixtures.oneRangeBucket));
expect(spiedTabify).to.have.property('callCount', 1);
expect(spiedTabify.firstCall.args[0].isHierarchical()).to.equal(true);
expect(vis.isHierarchical()).to.equal(true);
});
it('passes partialRows:false to tabify based on the vis params', function () {
// spy on the tabify private module
const spiedTabify = sinon.spy(Private(AggResponseTabifyProvider));
Private.stub(AggResponseTabifyProvider, spiedTabify);
const vis = new OneRangeVis({ showPartialRows: false });
initController(vis);
attachEsResponseToScope(spiedTabify(vis, fixtures.oneRangeBucket));
expect(spiedTabify).to.have.property('callCount', 1);
expect(spiedTabify.firstCall.args[0].isHierarchical()).to.equal(false);
expect(vis.isHierarchical()).to.equal(false);
});
});

View file

@ -5,12 +5,11 @@ import ngMock from 'ng_mock';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import FixturesAggRespGeohashGridProvider from 'fixtures/agg_resp/geohash_grid';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { AggResponseGeoJsonProvider } from 'ui/agg_response/geo_json/geo_json';
describe('GeoJson Agg Response Converter', function () {
let vis;
let tabify;
let convert;
let esResponse;
let expectedAggs;
@ -23,7 +22,6 @@ describe('GeoJson Agg Response Converter', function () {
const indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
esResponse = Private(FixturesAggRespGeohashGridProvider);
tabify = Private(AggResponseTabifyProvider);
convert = Private(AggResponseGeoJsonProvider);
createVis = function (useGeocentroid) {
@ -54,7 +52,7 @@ describe('GeoJson Agg Response Converter', function () {
[ { asAggConfigResults: true }, { asAggConfigResults: false } ].forEach(function (tableOpts) {
function makeTable() {
return _.sample(tabify(vis, esResponse, tableOpts).tables);
return _.sample(tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResponse, tableOpts).tables);
}
function makeSingleChart(table) {

View file

@ -1,13 +1,13 @@
import { BuildHierarchicalDataProvider } from 'ui/agg_response/hierarchical/build_hierarchical_data';
import { AggResponsePointSeriesProvider } from 'ui/agg_response/point_series/point_series';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { AggResponseGeoJsonProvider } from 'ui/agg_response/geo_json/geo_json';
export function AggResponseIndexProvider(Private) {
return {
hierarchical: Private(BuildHierarchicalDataProvider),
pointSeries: Private(AggResponsePointSeriesProvider),
tabify: Private(AggResponseTabifyProvider),
tabify: tabifyAggResponse,
geoJson: Private(AggResponseGeoJsonProvider)
};
}

View file

@ -4,7 +4,7 @@ import AggConfigResult from 'ui/vis/agg_config_result';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { VisProvider } from 'ui/vis';
import { AggResponseTabifyTableProvider } from 'ui/agg_response/tabify/_table';
import { TabifyTable } from 'ui/agg_response/tabify/_table';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { AggResponsePointSeriesProvider } from 'ui/agg_response/point_series/point_series';
@ -14,13 +14,11 @@ describe('pointSeriesChartDataFromTable', function () {
let pointSeriesChartDataFromTable;
let indexPattern;
let Table;
let Vis;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
Vis = Private(VisProvider);
Table = Private(AggResponseTabifyTableProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
pointSeriesChartDataFromTable = Private(AggResponsePointSeriesProvider);
}));
@ -30,7 +28,7 @@ describe('pointSeriesChartDataFromTable', function () {
const agg = vis.aggs[0];
const result = new AggConfigResult(vis.aggs[0], void 0, 100, 100);
const table = new Table();
const table = new TabifyTable();
table.columns = [ { aggConfig: agg } ];
table.rows.push([ result ]);
@ -69,7 +67,7 @@ describe('pointSeriesChartDataFromTable', function () {
};
const rowCount = 3;
const table = new Table();
const table = new TabifyTable();
table.columns = [ x.col, y.col ];
_.times(rowCount, function (i) {
const date = new AggConfigResult(x.agg, void 0, x.at(i));
@ -130,7 +128,7 @@ describe('pointSeriesChartDataFromTable', function () {
};
const rowCount = 3;
const table = new Table();
const table = new TabifyTable();
table.columns = [ date.col, avg.col, max.col ];
_.times(rowCount, function (i) {
const dateResult = new AggConfigResult(date.agg, void 0, date.at(i));
@ -209,7 +207,7 @@ describe('pointSeriesChartDataFromTable', function () {
const metricCount = 2;
const rowsPerSegment = 2;
const rowCount = extensions.length * rowsPerSegment;
const table = new Table();
const table = new TabifyTable();
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

@ -1,24 +1,16 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponseBucketsProvider } from 'ui/agg_response/tabify/_buckets';
import { TabifyBuckets } from 'ui/agg_response/tabify/_buckets';
describe('Buckets wrapper', function () {
let Buckets;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
Buckets = Private(AggResponseBucketsProvider);
}));
function test(aggResp, count, keys) {
it('reads the length', function () {
const buckets = new Buckets(aggResp);
const buckets = new TabifyBuckets(aggResp);
expect(buckets).to.have.length(count);
});
it('itterates properly, passing in the key', function () {
const buckets = new Buckets(aggResp);
const buckets = new TabifyBuckets(aggResp);
const keysSent = [];
buckets.forEach(function (bucket, key) {
keysSent.push(key);
@ -65,7 +57,7 @@ describe('Buckets wrapper', function () {
single_bucket: {},
doc_count: 5
};
const buckets = new Buckets(aggResp);
const buckets = new TabifyBuckets(aggResp);
expect(buckets).to.have.length(1);
});
});

View file

@ -1,16 +1,14 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponseGetColumnsProvider } from 'ui/agg_response/tabify/_get_columns';
import { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
describe('get columns', function () {
let getColumns;
let Vis;
let indexPattern;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
getColumns = Private(AggResponseGetColumnsProvider);
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));
@ -20,7 +18,7 @@ describe('get columns', function () {
type: 'pie'
});
while (vis.aggs.length) vis.aggs.pop();
const columns = getColumns(vis);
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
expect(columns).to.have.length(1);
expect(columns[0]).to.have.property('aggConfig');
@ -35,7 +33,7 @@ describe('get columns', function () {
]
});
const columns = getColumns(vis);
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
expect(columns).to.have.length(2);
expect(columns[1]).to.have.property('aggConfig');
@ -53,7 +51,7 @@ describe('get columns', function () {
]
});
const columns = getColumns(vis);
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
expect(columns).to.have.length(8);
columns.forEach(function (column, i) {
@ -75,7 +73,7 @@ describe('get columns', function () {
]
});
const columns = getColumns(vis);
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
function checkColumns(column, i) {
expect(column).to.have.property('aggConfig');
@ -111,7 +109,7 @@ describe('get columns', function () {
]
});
const columns = getColumns(vis);
const columns = tabifyGetColumns(vis.getAggConfig().getResponseAggs(), null, vis.isHierarchical());
expect(columns).to.have.length(6);
// sum should be last

View file

@ -2,18 +2,16 @@ import _ from 'lodash';
import fixtures from 'fixtures/fake_hierarchical_data';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
describe('tabifyAggResponse Integration', function () {
let Vis;
let indexPattern;
let tabifyAggResponse;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
tabifyAggResponse = Private(AggResponseTabifyProvider);
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));
@ -31,7 +29,10 @@ describe('tabifyAggResponse Integration', function () {
});
normalizeIds(vis);
const resp = tabifyAggResponse(vis, fixtures.metricOnly, { canSplit: false });
const resp = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.metricOnly, {
canSplit: false,
isHierarchical: vis.isHierarchical()
});
expect(resp).to.not.have.property('tables');
expect(resp).to.have.property('rows').and.property('columns');
@ -154,7 +155,7 @@ describe('tabifyAggResponse Integration', function () {
// only complete rows, and only put the metrics at the end.
vis.isHierarchical = _.constant(false);
const tabbed = tabifyAggResponse(vis, esResp);
const tabbed = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResp, { isHierarchical: vis.isHierarchical() });
expectRootGroup(tabbed, function expectTable(table, splitKey) {
expectColumns(table, [src, os, avg]);
@ -180,8 +181,9 @@ describe('tabifyAggResponse Integration', function () {
// the existing bucket and it's metric
vis.isHierarchical = _.constant(true);
const tabbed = tabifyAggResponse(vis, esResp, {
partialRows: true
const tabbed = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResp, {
partialRows: true,
isHierarchical: vis.isHierarchical()
});
expectRootGroup(tabbed, function expectTable(table, splitKey) {
@ -214,9 +216,10 @@ describe('tabifyAggResponse Integration', function () {
// the end
vis.isHierarchical = _.constant(true);
const tabbed = tabifyAggResponse(vis, esResp, {
const tabbed = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResp, {
partialRows: true,
minimalColumns: true
minimalColumns: true,
isHierarchical: vis.isHierarchical()
});
expectRootGroup(tabbed, function expectTable(table, splitKey) {
@ -246,8 +249,9 @@ describe('tabifyAggResponse Integration', function () {
// create metric columns after each bucket
vis.isHierarchical = _.constant(false);
const tabbed = tabifyAggResponse(vis, esResp, {
minimalColumns: false
const tabbed = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), esResp, {
minimalColumns: false,
isHierarchical: vis.isHierarchical()
});
expectRootGroup(tabbed, function expectTable(table) {

View file

@ -2,84 +2,44 @@ import _ from 'lodash';
import sinon from 'sinon';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { TabbedAggResponseWriterProvider } from 'ui/agg_response/tabify/_response_writer';
import { AggResponseTabifyTableGroupProvider } from 'ui/agg_response/tabify/_table_group';
import { AggResponseBucketsProvider } from 'ui/agg_response/tabify/_buckets';
import { AggResponseGetColumnsProvider } from 'ui/agg_response/tabify/_get_columns';
import { TabbedAggResponseWriter } from 'ui/agg_response/tabify/_response_writer';
import { TabifyTableGroup } from 'ui/agg_response/tabify/_table_group';
import { TabifyBuckets } from 'ui/agg_response/tabify/_buckets';
import { VisProvider } from 'ui/vis';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
describe('ResponseWriter class', function () {
describe('TabbedAggResponseWriter class', function () {
let Vis;
let Buckets;
let Private;
let TableGroup;
let getColumns;
let indexPattern;
let ResponseWriter;
function defineSetup(stubGetColumns) {
function defineSetup() {
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector) {
Private = $injector.get('Private');
if (stubGetColumns) {
getColumns = sinon.stub();
Private.stub(AggResponseGetColumnsProvider, getColumns);
}
ResponseWriter = Private(TabbedAggResponseWriterProvider);
TableGroup = Private(AggResponseTabifyTableGroupProvider);
Buckets = Private(AggResponseBucketsProvider);
Vis = Private(VisProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
}));
}
describe('Constructor', function () {
defineSetup(true);
it('gets the columns for the vis', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
new ResponseWriter(vis);
expect(getColumns).to.have.property('callCount', 1);
expect(getColumns.firstCall.args[0]).to.be(vis);
});
it('collects the aggConfigs from each column in aggStack', function () {
const aggs = [
{ type: 'date_histogram', schema: 'segment', params: { field: '@timestamp' } },
{ type: 'terms', schema: 'segment', params: { field: 'extension' } },
{ type: 'avg', schema: 'metric', params: { field: 'bytes' } }
];
getColumns.returns(aggs.map(function (agg) {
return { aggConfig: agg };
}));
const vis = new Vis(indexPattern, {
type: 'histogram',
aggs: aggs
});
const writer = new ResponseWriter(vis);
expect(writer.aggStack).to.be.an('array');
expect(writer.aggStack).to.have.length(aggs.length);
writer.aggStack.forEach(function (agg, i) {
expect(agg).to.be(aggs[i]);
});
});
defineSetup();
it('sets canSplit=true by default', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('canSplit', true);
});
it('sets canSplit=false when config says to', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new ResponseWriter(vis, { canSplit: false });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
canSplit: false,
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('canSplit', false);
});
@ -88,7 +48,10 @@ describe('ResponseWriter class', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const partial = Boolean(Math.round(Math.random()));
const writer = new ResponseWriter(vis, { partialRows: partial });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical(),
partialRows: partial
});
expect(writer).to.have.property('partialRows', partial);
});
@ -97,16 +60,20 @@ describe('ResponseWriter class', function () {
const hierarchical = Boolean(Math.round(Math.random()));
sinon.stub(vis, 'isHierarchical').returns(hierarchical);
const writer = new ResponseWriter(vis, {});
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
expect(writer).to.have.property('partialRows', hierarchical);
});
});
it('starts off with a root TableGroup', function () {
it('starts off with a root TabifyTableGroup', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new ResponseWriter(vis);
expect(writer.root).to.be.a(TableGroup);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
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);
@ -117,15 +84,20 @@ describe('ResponseWriter class', function () {
defineSetup();
describe('#response()', function () {
it('returns the root TableGroup if splitting', function () {
it('returns the root TabifyTableGroup if splitting', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
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 ResponseWriter(vis, { canSplit: false });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical(),
canSplit: false
});
const table = writer._table();
expect(writer.response()).to.be(table);
});
@ -138,8 +110,10 @@ describe('ResponseWriter class', function () {
{ type: 'count', schema: 'metric' }
]
});
const buckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const writer = new ResponseWriter(vis);
const buckets = new TabifyBuckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
const tables = [];
writer.split(vis.aggs[0], buckets, function () {
@ -153,7 +127,7 @@ describe('ResponseWriter class', function () {
});
const resp = writer.response();
expect(resp).to.be.a(TableGroup);
expect(resp).to.be.a(TabifyTableGroup);
expect(resp.tables).to.have.length(2);
const nginx = resp.tables.shift();
@ -190,8 +164,11 @@ describe('ResponseWriter class', function () {
]
});
const agg = vis.aggs.bySchemaName.split[0];
const buckets = new Buckets({ buckets: [ { key: 'apache' } ] });
const writer = new ResponseWriter(vis, { canSplit: false });
const buckets = new TabifyBuckets({ buckets: [ { key: 'apache' } ] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical(),
canSplit: false
});
expect(function () {
writer.split(agg, buckets, _.noop);
@ -209,10 +186,13 @@ describe('ResponseWriter class', function () {
]
});
const writer = new ResponseWriter(vis, { asAggConfigResults: true });
const extensions = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] });
const types = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const os = new Buckets({ buckets: [ { key: 'window' }, { key: 'osx' } ] });
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
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 () {
@ -254,10 +234,12 @@ describe('ResponseWriter class', function () {
});
describe('#cell()', function () {
it('logs a cell in the ResponseWriters row buffer, calls the block arg, then removes the value from the buffer',
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 ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
expect(writer.rowBuffer).to.have.length(0);
writer.cell({}, 500, function () {
@ -269,9 +251,11 @@ describe('ResponseWriter class', function () {
});
describe('#row()', function () {
it('writes the ResponseWriters internal rowBuffer into a table', function () {
it('writes the TabbedAggResponseWriters internal rowBuffer into a table', function () {
const vis = new Vis(indexPattern, { type: 'histogram', aggs: [] });
const writer = new ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
const table = writer._table();
writer.cell({}, 1, function () {
@ -299,20 +283,22 @@ describe('ResponseWriter class', function () {
const splits = vis.aggs.bySchemaName.split;
const type = splits[0];
const typeBuckets = new Buckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const typeTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'nginx' }, { key: 'apache' } ] });
const ext = splits[1];
const extBuckets = new Buckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] });
const extTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'jpg' }, { key: 'png' } ] });
const os = splits[2];
const osBuckets = new Buckets({ buckets: [ { key: 'windows' }, { key: 'mac' } ] });
const osTabifyBuckets = new TabifyBuckets({ buckets: [ { key: 'windows' }, { key: 'mac' } ] });
const count = vis.aggs[3];
const writer = new ResponseWriter(vis);
writer.split(type, typeBuckets, function () {
writer.split(ext, extBuckets, function () {
writer.split(os, osBuckets, function (bucket, key) {
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
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();
});
@ -353,7 +339,9 @@ describe('ResponseWriter class', function () {
]
});
const writer = new ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
const table = writer._table();
writer.cell(vis.aggs[0], 'apache', function () {
writer.row();
@ -372,7 +360,9 @@ describe('ResponseWriter class', function () {
]
});
const writer = new ResponseWriter(vis);
const writer = new TabbedAggResponseWriter(vis.getAggConfig().getResponseAggs(), {
isHierarchical: vis.isHierarchical()
});
const table = writer._table();
writer.cell(vis.aggs[0], 'apache', function () {
writer.row();

View file

@ -1,26 +1,17 @@
import _ from 'lodash';
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponseTabifyTableProvider } from 'ui/agg_response/tabify/_table';
describe('Table class', function () {
let Table;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
Table = Private(AggResponseTabifyTableProvider);
}));
import { TabifyTable } from 'ui/agg_response/tabify/_table';
describe('TabifyTable class', function () {
it('exposes rows array, but not the columns', function () {
const table = new Table();
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 Table();
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: football
@ -32,31 +23,31 @@ describe('Table class', function () {
it('throws a TypeError if the column is malformed', function () {
expect(function () {
const notAColumn = {};
(new Table()).aggConfig(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 Table();
const table = new TabifyTable();
expect(table.title()).to.be('');
});
it('returns the title of the TableGroup if the table is part of one', function () {
const table = new Table();
it('returns the title of the TabifyTableGroup if the table is part of one', function () {
const table = new TabifyTable();
table.$parent = {
title: 'TableGroup Title',
title: 'TabifyTableGroup Title',
tables: [table]
};
expect(table.title()).to.be('TableGroup Title');
expect(table.title()).to.be('TabifyTableGroup Title');
});
});
describe('#field', function () {
it('calls the columns aggConfig#getField() method', function () {
const table = new Table();
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: {
@ -70,7 +61,7 @@ describe('Table class', function () {
describe('#fieldFormatter', function () {
it('calls the columns aggConfig#fieldFormatter() method', function () {
const table = new Table();
const table = new TabifyTable();
const football = {};
const column = {
aggConfig: {

View file

@ -1,17 +1,10 @@
import expect from 'expect.js';
import ngMock from 'ng_mock';
import { AggResponseTabifyTableGroupProvider } from 'ui/agg_response/tabify/_table_group';
import { TabifyTableGroup } from 'ui/agg_response/tabify/_table_group';
describe('Table Group class', function () {
let TableGroup;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
TableGroup = Private(AggResponseTabifyTableGroupProvider);
}));
it('exposes tables array and empty aggConfig, key and title', function () {
const tableGroup = new TableGroup();
const tableGroup = new TabifyTableGroup();
expect(tableGroup.tables).to.be.an('array');
expect(tableGroup.aggConfig).to.be(null);
expect(tableGroup.key).to.be(null);

View file

@ -1,68 +1,65 @@
import _ from 'lodash';
export function AggResponseBucketsProvider() {
function Buckets(aggResp, aggParams) {
if (_.has(aggResp, 'buckets')) {
this.buckets = aggResp.buckets;
} else if (aggResp) {
// Some Bucket Aggs only return a single bucket (like filter).
// In those instances, the aggResp is the content of the single bucket.
this.buckets = [aggResp];
} else {
this.buckets = [];
}
this.objectMode = _.isPlainObject(this.buckets);
if (this.objectMode) {
this._keys = _.keys(this.buckets);
this.length = this._keys.length;
} else {
this.length = this.buckets.length;
}
if (this.length && aggParams) this._orderBucketsAccordingToParams(aggParams);
function TabifyBuckets(aggResp, aggParams) {
if (_.has(aggResp, 'buckets')) {
this.buckets = aggResp.buckets;
} else if (aggResp) {
// Some Bucket Aggs only return a single bucket (like filter).
// In those instances, the aggResp is the content of the single bucket.
this.buckets = [aggResp];
} else {
this.buckets = [];
}
Buckets.prototype.forEach = function (fn) {
const buckets = this.buckets;
this.objectMode = _.isPlainObject(this.buckets);
if (this.objectMode) {
this._keys = _.keys(this.buckets);
this.length = this._keys.length;
} else {
this.length = this.buckets.length;
}
if (this.objectMode) {
this._keys.forEach(function (key) {
fn(buckets[key], key);
});
} else {
buckets.forEach(function (bucket) {
fn(bucket, bucket.key);
});
}
};
Buckets.prototype._isRangeEqual = function (range1, range2) {
return _.get(range1, 'from', null) === _.get(range2, 'from', null)
&& _.get(range1, 'to', null) === _.get(range2, 'to', null);
};
Buckets.prototype._orderBucketsAccordingToParams = function (params) {
if (params.filters && this.objectMode) {
this._keys = params.filters.map(filter => {
return filter.label || filter.input.query || '*';
});
} else if (params.ranges && this.objectMode) {
this._keys = params.ranges.map(range => {
return _.findKey(this.buckets, el => this._isRangeEqual(el, range));
});
} else if (params.ranges && params.field.type !== 'date') {
let ranges = params.ranges;
if (params.ipRangeType) {
ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo;
}
this.buckets = ranges.map(range => {
if (range.mask) return this.buckets.find(el => el.key === range.mask);
return this.buckets.find(el => this._isRangeEqual(el, range));
});
}
};
return Buckets;
if (this.length && aggParams) this._orderBucketsAccordingToParams(aggParams);
}
TabifyBuckets.prototype.forEach = function (fn) {
const buckets = this.buckets;
if (this.objectMode) {
this._keys.forEach(function (key) {
fn(buckets[key], key);
});
} else {
buckets.forEach(function (bucket) {
fn(bucket, bucket.key);
});
}
};
TabifyBuckets.prototype._isRangeEqual = function (range1, range2) {
return _.get(range1, 'from', null) === _.get(range2, 'from', null)
&& _.get(range1, 'to', null) === _.get(range2, 'to', null);
};
TabifyBuckets.prototype._orderBucketsAccordingToParams = function (params) {
if (params.filters && this.objectMode) {
this._keys = params.filters.map(filter => {
return filter.label || filter.input.query || '*';
});
} else if (params.ranges && this.objectMode) {
this._keys = params.ranges.map(range => {
return _.findKey(this.buckets, el => this._isRangeEqual(el, range));
});
} else if (params.ranges && params.field.type !== 'date') {
let ranges = params.ranges;
if (params.ipRangeType) {
ranges = params.ipRangeType === 'mask' ? ranges.mask : ranges.fromTo;
}
this.buckets = ranges.map(range => {
if (range.mask) return this.buckets.find(el => el.key === range.mask);
return this.buckets.find(el => this._isRangeEqual(el, range));
});
}
};
export { TabifyBuckets };

View file

@ -1,51 +1,38 @@
import _ from 'lodash';
import { VisAggConfigProvider } from 'ui/vis/agg_config';
export function AggResponseGetColumnsProvider(Private) {
const AggConfig = Private(VisAggConfigProvider);
export function tabifyGetColumns(aggs, minimal, hierarchical) {
return function getColumns(vis, minimal) {
const aggs = vis.getAggConfig().getResponseAggs();
if (minimal == null) minimal = !hierarchical;
if (minimal == null) minimal = !vis.isHierarchical();
if (!vis.getAggConfig().bySchemaGroup.metrics) {
aggs.push(new AggConfig(vis, {
type: 'count',
schema: vis.type.schemas.metrics[0].name
}));
}
// pick the columns
if (minimal) {
return aggs.map(function (agg) {
return { aggConfig: agg };
});
}
// supposed to be bucket,...metrics,bucket,...metrics
const columns = [];
// seperate the metrics
const grouped = _.groupBy(aggs, function (agg) {
return agg.schema.group;
// pick the columns
if (minimal) {
return aggs.map(function (agg) {
return { aggConfig: agg };
});
}
if (!grouped.buckets) {
// return just the metrics, in column format
return grouped.metrics.map(function (agg) {
return { aggConfig: agg };
});
}
// supposed to be bucket,...metrics,bucket,...metrics
const columns = [];
// return the buckets, and after each place all of the metrics
grouped.buckets.forEach(function (agg) {
columns.push({ aggConfig: agg });
grouped.metrics.forEach(function (metric) {
columns.push({ aggConfig: metric });
});
// seperate the metrics
const grouped = _.groupBy(aggs, function (agg) {
return agg.schema.group;
});
if (!grouped.buckets) {
// return just the metrics, in column format
return grouped.metrics.map(function (agg) {
return { aggConfig: agg };
});
}
return columns;
};
// return the buckets, and after each place all of the metrics
grouped.buckets.forEach(function (agg) {
columns.push({ aggConfig: agg });
grouped.metrics.forEach(function (metric) {
columns.push({ aggConfig: metric });
});
});
return columns;
}

View file

@ -1,288 +1,280 @@
import _ from 'lodash';
import AggConfigResult from 'ui/vis/agg_config_result';
import { AggResponseTabifyTableProvider } from 'ui/agg_response/tabify/_table';
import { AggResponseTabifyTableGroupProvider } from 'ui/agg_response/tabify/_table_group';
import { AggResponseGetColumnsProvider } from 'ui/agg_response/tabify/_get_columns';
import { TabifyTable } from 'ui/agg_response/tabify/_table';
import { TabifyTableGroup } from 'ui/agg_response/tabify/_table_group';
import { tabifyGetColumns } from 'ui/agg_response/tabify/_get_columns';
export function TabbedAggResponseWriterProvider(Private) {
const Table = Private(AggResponseTabifyTableProvider);
const TableGroup = Private(AggResponseTabifyTableGroupProvider);
const getColumns = Private(AggResponseGetColumnsProvider);
_.class(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
*/
function TabbedAggResponseWriter(vis, opts) {
this.vis = vis;
this.opts = opts || {};
this.rowBuffer = [];
const visIsHier = vis.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;
this.columns = getColumns(vis, this.minimalColumns);
this.aggStack = _.pluck(this.columns, 'aggConfig');
this.root = new TableGroup();
this.acrStack = [];
this.splitStack = [this.root];
}
/**
* 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) ? TableGroup : Table;
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;
};
/**
* 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
*/
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;
});
// we must have already removed this column
if (i === -1) return;
this.columns.splice(i, 1);
if (this.minimalColumns) return;
// hierarchical vis creats 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;
});
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 staskResult = this.asAggConfigResults && value.type === 'bucket';
this.rowBuffer.push(value);
if (staskResult) this.acrStack.unshift(value);
if (_.isFunction(block)) block.call(this);
this.rowBuffer.pop(value);
if (staskResult) 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) {
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;
};
/**
* Get the actual response
*
* @return {object} - the final table-tree
*/
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 TabbedAggResponseWriter;
_.class(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
*/
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;
this.columns = tabifyGetColumns(aggs, this.minimalColumns);
this.aggStack = _.pluck(this.columns, 'aggConfig');
this.root = new TabifyTableGroup();
this.acrStack = [];
this.splitStack = [this.root];
}
/**
* 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;
};
/**
* 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
*/
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;
});
// we must have already removed this column
if (i === -1) return;
this.columns.splice(i, 1);
if (this.minimalColumns) return;
// hierarchical vis creats 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;
});
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 staskResult = this.asAggConfigResults && value.type === 'bucket';
this.rowBuffer.push(value);
if (staskResult) this.acrStack.unshift(value);
if (_.isFunction(block)) block.call(this);
this.rowBuffer.pop(value);
if (staskResult) 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) {
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;
};
/**
* Get the actual response
*
* @return {object} - the final table-tree
*/
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;
};
export { TabbedAggResponseWriter };

View file

@ -1,37 +1,34 @@
export function AggResponseTabifyTableProvider() {
/**
* 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 Table() {
this.columns = null; // written with the first row
this.rows = [];
}
Table.prototype.title = function () {
if (this.$parent) {
return this.$parent.title;
} else {
return '';
}
};
Table.prototype.aggConfig = function (col) {
if (!col.aggConfig) {
throw new TypeError('Column is missing the aggConfig property');
}
return col.aggConfig;
};
Table.prototype.field = function (col) {
return this.aggConfig(col).getField();
};
Table.prototype.fieldFormatter = function (col) {
return this.aggConfig(col).fieldFormatter();
};
return Table;
/**
* 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,23 +1,20 @@
export function AggResponseTabifyTableGroupProvider() {
/**
* 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 TableGroup() {
this.aggConfig = null;
this.key = null;
this.title = null;
this.tables = [];
}
TableGroup.prototype.field = function () {
if (this.aggConfig) return this.aggConfig.getField();
};
TableGroup.prototype.fieldFormatter = function () {
if (this.aggConfig) return this.aggConfig.fieldFormatter();
};
return TableGroup;
/**
* 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

@ -1 +1 @@
export { AggResponseTabifyProvider } from './tabify';
export { tabifyAggResponse } from './tabify';

View file

@ -1,109 +1,102 @@
import _ from 'lodash';
import { TabbedAggResponseWriterProvider } from 'ui/agg_response/tabify/_response_writer';
import { AggResponseBucketsProvider } from 'ui/agg_response/tabify/_buckets';
import { TabbedAggResponseWriter } from 'ui/agg_response/tabify/_response_writer';
import { TabifyBuckets } from 'ui/agg_response/tabify/_buckets';
export function AggResponseTabifyProvider(Private, Notifier) {
const TabbedAggResponseWriter = Private(TabbedAggResponseWriterProvider);
const Buckets = Private(AggResponseBucketsProvider);
const notify = new Notifier({ location: 'agg_response/tabify' });
export function tabifyAggResponse(aggs, esResponse, respOpts = {}) {
const write = new TabbedAggResponseWriter(aggs, respOpts);
function tabifyAggResponse(vis, esResponse, respOpts) {
const write = new TabbedAggResponseWriter(vis, respOpts);
const topLevelBucket = _.assign({}, esResponse.aggregations, {
doc_count: esResponse.hits.total
});
const topLevelBucket = _.assign({}, esResponse.aggregations, {
doc_count: esResponse.hits.total
});
collectBucket(write, topLevelBucket, '', 1);
collectBucket(write, topLevelBucket, '', 1);
return write.response();
}
/**
* read an aggregation from a bucket, which is *might* be found at key (if
* the response came in object form), and will recurse down the aggregation
* tree and will pass the read values to the ResponseWriter.
*
* @param {object} bucket - a bucket from the aggResponse
* @param {undefined|string} key - the key where the bucket was found
* @returns {undefined}
*/
function collectBucket(write, bucket, key, aggScale) {
const agg = write.aggStack.shift();
const aggInfo = agg.write();
aggScale *= aggInfo.metricScale || 1;
switch (agg.schema.group) {
case 'buckets':
const buckets = new Buckets(bucket[agg.id], agg.params);
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) {
// 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);
passEmptyBuckets(write, bucket, key, aggScale);
write.aggStack.shift();
} else {
// we don't have any buckets, and we don't have isHierarchical
// data, so no metrics, just try to write the row
write.row();
}
break;
case 'metrics':
let value = agg.getValue(bucket);
// since the aggregation could be a non integer (such as a max date)
// only do the scaling calculation if it is needed.
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);
}
});
break;
}
write.aggStack.unshift(agg);
}
// 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();
switch (agg.schema.group) {
case 'metrics':
// pass control back to collectBucket()
write.aggStack.unshift(agg);
collectBucket(write, bucket, key, aggScale);
return;
case 'buckets':
write.cell(agg, '', function () {
passEmptyBuckets(write, bucket, key, aggScale);
});
}
write.aggStack.unshift(agg);
}
return notify.timed('tabify agg response', tabifyAggResponse);
return write.response();
}
/**
* read an aggregation from a bucket, which is *might* be found at key (if
* the response came in object form), and will recurse down the aggregation
* tree and will pass the read values to the ResponseWriter.
*
* @param {object} bucket - a bucket from the aggResponse
* @param {undefined|string} key - the key where the bucket was found
* @returns {undefined}
*/
function collectBucket(write, bucket, key, aggScale) {
const agg = write.aggStack.shift();
const aggInfo = agg.write();
aggScale *= aggInfo.metricScale || 1;
switch (agg.schema.group) {
case 'buckets':
const buckets = new TabifyBuckets(bucket[agg.id], agg.params);
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) {
// 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);
passEmptyBuckets(write, bucket, key, aggScale);
write.aggStack.shift();
} else {
// we don't have any buckets, and we don't have isHierarchical
// data, so no metrics, just try to write the row
write.row();
}
break;
case 'metrics':
let value = agg.getValue(bucket);
// since the aggregation could be a non integer (such as a max date)
// only do the scaling calculation if it is needed.
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);
}
});
break;
}
write.aggStack.unshift(agg);
}
// 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();
switch (agg.schema.group) {
case 'metrics':
// pass control back to collectBucket()
write.aggStack.unshift(agg);
collectBucket(write, bucket, key, aggScale);
return;
case 'buckets':
write.cell(agg, '', function () {
passEmptyBuckets(write, bucket, key, aggScale);
});
}
write.aggStack.unshift(agg);
}

View file

@ -2,20 +2,18 @@ import $ from 'jquery';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import fixtures from 'fixtures/fake_hierarchical_data';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from 'ui/vis';
describe('AggTableGroup Directive', function () {
let $rootScope;
let $compile;
let tabifyAggResponse;
let Vis;
let indexPattern;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private) {
tabifyAggResponse = Private(AggResponseTabifyProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
Vis = Private(VisProvider);
@ -34,7 +32,7 @@ describe('AggTableGroup Directive', function () {
it('renders a simple split response properly', function () {
const vis = new Vis(indexPattern, 'table');
$scope.group = tabifyAggResponse(vis, fixtures.metricOnly);
$scope.group = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.metricOnly);
$scope.sort = {
columnIndex: null,
direction: null
@ -76,7 +74,7 @@ describe('AggTableGroup Directive', function () {
agg.id = 'agg_' + (i + 1);
});
const group = $scope.group = tabifyAggResponse(vis, fixtures.threeTermBuckets);
const group = $scope.group = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.threeTermBuckets);
const $el = $('<kbn-agg-table-group group="group"></kbn-agg-table-group>');
$compile($el)($scope);
$scope.$digest();

View file

@ -5,21 +5,19 @@ import ngMock from 'ng_mock';
import expect from 'expect.js';
import fixtures from 'fixtures/fake_hierarchical_data';
import sinon from 'sinon';
import { AggResponseTabifyProvider } from 'ui/agg_response/tabify/tabify';
import { tabifyAggResponse } from 'ui/agg_response/tabify/tabify';
import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern';
import { VisProvider } from 'ui/vis';
describe('AggTable Directive', function () {
let $rootScope;
let $compile;
let tabifyAggResponse;
let Vis;
let indexPattern;
let settings;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function ($injector, Private, config) {
tabifyAggResponse = Private(AggResponseTabifyProvider);
indexPattern = Private(FixturesStubbedLogstashIndexPatternProvider);
Vis = Private(VisProvider);
settings = config;
@ -39,7 +37,11 @@ describe('AggTable Directive', function () {
it('renders a simple response properly', function () {
const vis = new Vis(indexPattern, 'table');
$scope.table = tabifyAggResponse(vis, fixtures.metricOnly, { canSplit: false });
$scope.table = tabifyAggResponse(
vis.getAggConfig().getResponseAggs(),
fixtures.metricOnly,
{ canSplit: false, hierarchical: vis.isHierarchical() }
);
const $el = $compile('<kbn-agg-table table="table"></kbn-agg-table>')($scope);
$scope.$digest();
@ -71,7 +73,10 @@ describe('AggTable Directive', function () {
agg.id = 'agg_' + (i + 1);
});
$scope.table = tabifyAggResponse(vis, fixtures.threeTermBuckets, { canSplit: false });
$scope.table = tabifyAggResponse(vis.getAggConfig().getResponseAggs(), fixtures.threeTermBuckets, {
canSplit: false,
isHierarchical: vis.isHierarchical()
});
const $el = $('<kbn-agg-table table="table"></kbn-agg-table>');
$compile($el)($scope);
$scope.$digest();
@ -134,7 +139,7 @@ describe('AggTable Directive', function () {
const oldTimezoneSetting = settings.get('dateFormat:tz');
settings.set('dateFormat:tz', 'UTC');
$scope.table = tabifyAggResponse(vis,
$scope.table = tabifyAggResponse(vis.getAggConfig().getResponseAggs(),
fixtures.oneTermOneHistogramBucketWithTwoMetricsOneTopHitOneDerivative,
{ canSplit: false, minimalColumns: true, asAggConfigResults: true }
);

View file

@ -2,18 +2,16 @@ import _ from 'lodash';
import ngMock from 'ng_mock';
import expect from 'expect.js';
import sinon from 'sinon';
import { AggResponseTabifyTableProvider } from 'ui/agg_response/tabify/_table';
import { TabifyTable } from 'ui/agg_response/tabify/_table';
import { AggResponseIndexProvider } from 'ui/agg_response/index';
import { BasicResponseHandlerProvider } from 'ui/vis/response_handlers/basic';
describe('renderbot#buildChartData', function () {
let buildChartData;
let aggResponse;
let Table;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
Table = Private(AggResponseTabifyTableProvider);
aggResponse = Private(AggResponseIndexProvider);
buildChartData = Private(BasicResponseHandlerProvider).handler;
}));
@ -62,7 +60,7 @@ describe('renderbot#buildChartData', function () {
}
};
const esResp = { hits: { total: 1 } };
const tabbed = { tables: [ new Table() ] };
const tabbed = { tables: [ new TabifyTable() ] };
sinon.stub(aggResponse, 'tabify').returns(tabbed);
expect(buildChartData.call(renderbot, esResp)).to.eql(chart);
@ -71,7 +69,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 Table(), new Table(), new Table(), new Table()];
const tables = [new TabifyTable(), new TabifyTable(), new TabifyTable(), new TabifyTable()];
const renderbot = {
vis: {

View file

@ -1,16 +1,15 @@
import { AggResponseIndexProvider } from 'ui/agg_response/index';
import { AggResponseTabifyTableProvider } from 'ui/agg_response/tabify/_table';
import { TabifyTable } from 'ui/agg_response/tabify/_table';
import { VisResponseHandlersRegistryProvider } from 'ui/registry/vis_response_handlers';
const BasicResponseHandlerProvider = function (Private) {
const aggResponse = Private(AggResponseIndexProvider);
const Table = Private(AggResponseTabifyTableProvider);
function convertTableGroup(vis, tableGroup) {
const tables = tableGroup.tables;
const firstChild = tables[0];
if (firstChild instanceof Table) {
if (firstChild instanceof TabifyTable) {
const chart = convertTable(vis, firstChild);
// if chart is within a split, assign group title to its label
@ -53,9 +52,10 @@ const BasicResponseHandlerProvider = function (Private) {
resolve(aggResponse.hierarchical(vis, response));
}
const tableGroup = aggResponse.tabify(vis, response, {
const tableGroup = aggResponse.tabify(vis.getAggConfig().getResponseAggs(), response, {
canSplit: true,
asAggConfigResults: true
asAggConfigResults: true,
isHierarchical: vis.isHierarchical()
});
let converted = convertTableGroup(vis, tableGroup);

View file

@ -10,9 +10,10 @@ const TabifyResponseHandlerProvider = function (Private) {
handler: function (vis, response) {
return new Promise((resolve) => {
const tableGroup = aggResponse.tabify(vis, response, {
const tableGroup = aggResponse.tabify(vis.getAggConfig().getResponseAggs(), response, {
canSplit: true,
asAggConfigResults: _.get(vis, 'type.responseHandlerConfig.asAggConfigResults', false)
asAggConfigResults: _.get(vis, 'type.responseHandlerConfig.asAggConfigResults', false),
isHierarchical: vis.isHierarchical()
});
resolve(tableGroup);

View file

@ -46,7 +46,9 @@ describe('visualization_editor directive', function () {
vis.aggs.forEach(function (agg, i) { agg.id = 'agg_' + (i + 1); });
$rootScope.vis = vis;
$rootScope.visData = aggResponse.tabify(vis, esResponse);
$rootScope.visData = aggResponse.tabify(vis.getAggConfig().getResponseAggs(), esResponse, {
isHierarchical: vis.isHierarchical()
});
$rootScope.uiState = require('fixtures/mock_ui_state');
$rootScope.searchSource = searchSource;
$el = $('<visualization-editor vis="vis" vis-data="visData" ui-state="uiState" search-source="searchSource">');