@spalger/kibana
Version:
Kibana is an open source (Apache Licensed), browser based analytics and search dashboard for Elasticsearch. Kibana is a snap to setup and start using. Kibana strives to be easy to get started with, while also being flexible and powerful, just like Elastic
283 lines (227 loc) • 9.58 kB
JavaScript
define(function (require) {
return function TabbedAggResponseWriterProvider(Private) {
var _ = require('lodash');
var Table = Private(require('ui/agg_response/tabify/_table'));
var TableGroup = Private(require('ui/agg_response/tabify/_table_group'));
var getColumns = Private(require('ui/agg_response/tabify/_get_columns'));
var AggConfigResult = require('ui/Vis/AggConfigResult');
_.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 = [];
var 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) {
var Class = (group) ? TableGroup : Table;
var table = new Class();
var parent = this.splitStack[0];
if (group) {
table.aggConfig = agg;
table.key = key;
table.title = (table.fieldFormatter()(key)) + ': ' + 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) {
var 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
var 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);
var 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) {
var 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
var mCol = this.columns.splice(i, 1).pop();
var 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) {
var oldList = this.acrStack;
var newList = this.acrStack = [];
// walk from right to left through the old stack
// and move things to the new stack
var 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) {
var 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);
}
var newAcr = new AggConfigResult(acr.aggConfig, newList[0], acr.value, acr.aggConfig.getKey(acr));
newList.unshift(newAcr);
// and replace the acr in the row buffer if its there
var 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) {
if (this.asAggConfigResults) {
value = new AggConfigResult(agg, this.acrStack[0], value, value);
}
var 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) {
var cells = buffer || this.rowBuffer.slice(0);
if (!this.partialRows && cells.length < this.columns.length) {
return;
}
var split = this.splitStack[0];
var 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 () {
var 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, group) {
if (table.tables) table.tables.forEach(step);
else table.columns = columns.slice(0);
}(this.root));
if (this.canSplit) return this.root;
var table = this.root.tables[0];
if (!table) return;
delete table.$parent;
return table;
};
return TabbedAggResponseWriter;
};
});