mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 01:13:23 -04:00
Visualize skeleton in place
This commit is contained in:
parent
0b11291f59
commit
38d9115195
26 changed files with 1175 additions and 29 deletions
140
src/kibana/apps/visualize/controllers/visualize.js
Normal file
140
src/kibana/apps/visualize/controllers/visualize.js
Normal file
|
@ -0,0 +1,140 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
var app = require('modules').get('app/visualize');
|
||||
|
||||
require('../factories/vis');
|
||||
require('../services/aggs');
|
||||
|
||||
app.controller('Visualize', function ($scope, courier, createNotifier, Vis, Aggs) {
|
||||
var notify = createNotifier({
|
||||
location: 'Visualize Controller'
|
||||
});
|
||||
|
||||
// the object detailing the visualization
|
||||
var vis = $scope.vis = window.vis = new Vis({
|
||||
metric: {
|
||||
label: 'Y-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
segment: {
|
||||
label: 'X-Axis',
|
||||
min: 1,
|
||||
max: 1
|
||||
},
|
||||
group: {
|
||||
label: 'Color',
|
||||
max: 10
|
||||
},
|
||||
split: {
|
||||
label: 'Rows & Columns',
|
||||
max: 2
|
||||
}
|
||||
}, {
|
||||
split: [
|
||||
{
|
||||
field: 'response',
|
||||
size: 5,
|
||||
agg: 'terms'
|
||||
},
|
||||
{
|
||||
field: '_type',
|
||||
size: 5,
|
||||
agg: 'terms'
|
||||
}
|
||||
],
|
||||
segment: [
|
||||
{
|
||||
field: '@timestamp',
|
||||
interval: 'week'
|
||||
}
|
||||
],
|
||||
group: [
|
||||
{
|
||||
field: 'extension',
|
||||
size: 5,
|
||||
agg: 'terms',
|
||||
global: true
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
vis.dataSource.$scope($scope);
|
||||
|
||||
$scope.refreshFields = function () {
|
||||
$scope.fields = null;
|
||||
vis.dataSource.clearFieldCache().then(getFields, notify.error);
|
||||
};
|
||||
|
||||
function getFields() {
|
||||
vis.dataSource.getFields(function (err, fieldsHash) {
|
||||
if (err) return notify.error(err);
|
||||
|
||||
// create a sorted list of the fields for display purposes
|
||||
$scope.fields = _(fieldsHash)
|
||||
.keys()
|
||||
.sort()
|
||||
.transform(function (fields, name) {
|
||||
var field = fieldsHash[name];
|
||||
field.name = name;
|
||||
fields.push(field);
|
||||
})
|
||||
.value();
|
||||
|
||||
$scope.fields.byName = fieldsHash;
|
||||
});
|
||||
}
|
||||
// get the fields for initial display
|
||||
getFields();
|
||||
|
||||
$scope.Vis = Vis;
|
||||
$scope.Aggs = Aggs;
|
||||
|
||||
$scope.updateDataSource = function () {
|
||||
notify.event('update data source');
|
||||
var config = _.groupBy(vis.getConfig(), function (config) {
|
||||
switch (config.categoryName) {
|
||||
case 'group':
|
||||
case 'segment':
|
||||
return 'dimension';
|
||||
default:
|
||||
return config.categoryName;
|
||||
}
|
||||
});
|
||||
|
||||
if (!config.dimension) {
|
||||
// use the global aggregation if we don't have any dimensions
|
||||
config.dimension = [{
|
||||
agg: 'global'
|
||||
}];
|
||||
}
|
||||
|
||||
var dsl = {};
|
||||
var i = 0;
|
||||
|
||||
var nest = (function () {
|
||||
var current = dsl;
|
||||
return function (config) {
|
||||
current.aggs = {};
|
||||
var key = '_agg_' + (i++);
|
||||
|
||||
var aggDsl = {};
|
||||
aggDsl[config.agg] = config.aggParams;
|
||||
|
||||
current = current.aggs[key] = aggDsl;
|
||||
};
|
||||
}());
|
||||
|
||||
config.split && config.split.forEach(nest);
|
||||
config.dimension && config.dimension.forEach(nest);
|
||||
config.metric && config.metric.forEach(nest);
|
||||
|
||||
notify.log('config', config);
|
||||
notify.log('aggs', dsl.aggs);
|
||||
|
||||
vis.dataSource.aggs(dsl.aggs).fetch();
|
||||
notify.event('update data source', true);
|
||||
};
|
||||
|
||||
});
|
||||
});
|
25
src/kibana/apps/visualize/directives/canvas.js
Normal file
25
src/kibana/apps/visualize/directives/canvas.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
define(function (require) {
|
||||
var module = require('modules').get('app/visualize');
|
||||
var $ = require('jquery');
|
||||
|
||||
module.directive('visCanvas', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function ($scope, $el) {
|
||||
var $window = $(window);
|
||||
var $header = $('.content > nav.navbar:first()');
|
||||
|
||||
function stretchVis() {
|
||||
$el.css('height', $window.height() - $header.height() - 30);
|
||||
}
|
||||
|
||||
stretchVis();
|
||||
|
||||
$window.on('resize', stretchVis);
|
||||
$scope.$on('$destroy', function () {
|
||||
$window.off('resize', stretchVis);
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
22
src/kibana/apps/visualize/directives/config_category.js
Normal file
22
src/kibana/apps/visualize/directives/config_category.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
define(function (require) {
|
||||
var html = require('text!../partials/config_category.html');
|
||||
|
||||
require('./config_editor');
|
||||
|
||||
require('modules')
|
||||
.get('app/visualize')
|
||||
.directive('visConfigCategory', function () {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
categoryName: '=',
|
||||
vis: '=',
|
||||
fields: '='
|
||||
},
|
||||
template: html,
|
||||
link: function ($scope, $el) {
|
||||
$scope.category = $scope.vis[$scope.categoryName];
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
58
src/kibana/apps/visualize/directives/config_controlls.js
Normal file
58
src/kibana/apps/visualize/directives/config_controlls.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('app/visualize');
|
||||
var _ = require('lodash');
|
||||
|
||||
var templates = {
|
||||
orderAndSize: require('text!../partials/controls/order_and_size.html'),
|
||||
interval: require('text!../partials/controls/interval.html'),
|
||||
globalLocal: require('text!../partials/controls/global_local.html')
|
||||
};
|
||||
|
||||
app.directive('visConfigControls', function ($compile, Vis, Aggs) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
config: '='
|
||||
},
|
||||
link: function ($scope, $el, attr) {
|
||||
var $controls = $el.find('.agg-param-controls');
|
||||
|
||||
$scope.$watch('config.agg', function (aggName) {
|
||||
var agg = Aggs.aggsByName[aggName];
|
||||
var controlsHtml = '';
|
||||
|
||||
if (agg) {
|
||||
var aggParams = $scope.aggParams = agg.params;
|
||||
|
||||
_.forOwn(aggParams, function (param, name) {
|
||||
// if the param doesn't have options, or a default value, skip it
|
||||
if (!param.options) return;
|
||||
// if there isn't currently a value, or the current value is not one of the options, set it to the default
|
||||
if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
|
||||
$scope.config[name] = param.default;
|
||||
}
|
||||
});
|
||||
|
||||
if (aggParams.order && aggParams.size) {
|
||||
controlsHtml += ' ' + templates.orderAndSize;
|
||||
}
|
||||
|
||||
if (aggParams.interval) {
|
||||
controlsHtml += ' ' + templates.interval;
|
||||
}
|
||||
|
||||
if ($scope.config.categoryName === 'group') {
|
||||
controlsHtml += ' ' + templates.globalLocal;
|
||||
}
|
||||
}
|
||||
|
||||
$controls.html($compile(controlsHtml)($scope));
|
||||
});
|
||||
|
||||
$scope.Aggs = Aggs;
|
||||
$scope.Vis = Vis;
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
133
src/kibana/apps/visualize/directives/config_editor.js
Normal file
133
src/kibana/apps/visualize/directives/config_editor.js
Normal file
|
@ -0,0 +1,133 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('app/visualize');
|
||||
var _ = require('lodash');
|
||||
var $ = require('jquery');
|
||||
|
||||
require('filters/field_type');
|
||||
|
||||
app.directive('visConfigEditor', function ($compile, Vis, Aggs) {
|
||||
var categoryOptions = {
|
||||
metric: {
|
||||
template: require('text!../partials/editor/metric.html')
|
||||
},
|
||||
segment: {
|
||||
template: require('text!../partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
},
|
||||
group: {
|
||||
template: require('text!../partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
},
|
||||
split: {
|
||||
template: require('text!../partials/editor/dimension.html'),
|
||||
setup: setupDimension
|
||||
}
|
||||
};
|
||||
|
||||
var controlTemplates = {
|
||||
orderAndSize: require('text!../partials/controls/order_and_size.html'),
|
||||
interval: require('text!../partials/controls/interval.html'),
|
||||
globalLocal: require('text!../partials/controls/global_local.html')
|
||||
};
|
||||
|
||||
// generalized setup for group and segment
|
||||
function setupDimension($scope, $el) {
|
||||
var $controls = $el.find('.agg-param-controls');
|
||||
|
||||
function getAvailableAggsForField() {
|
||||
if (!$scope.config.field || !$scope.fields) return;
|
||||
|
||||
var field = $scope.fields.byName[$scope.config.field];
|
||||
|
||||
// clear the previous choices
|
||||
$scope.availableAggs = void 0;
|
||||
// get the new choices
|
||||
var aggs = Aggs.aggsByFieldType[field.type];
|
||||
|
||||
if (!aggs || aggs.length === 0) {
|
||||
// init or invalid field type
|
||||
$scope.config.agg = void 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (aggs.length === 1) {
|
||||
// only once choice, make it for the user
|
||||
$scope.config.agg = aggs[0].name;
|
||||
return;
|
||||
}
|
||||
|
||||
// set the new choices
|
||||
$scope.availableAggs = aggs;
|
||||
|
||||
// update the agg only if it is not currently a valid option
|
||||
if (!$scope.config.agg || !_.find(aggs, { name: $scope.config.agg })) {
|
||||
$scope.config.agg = aggs[0].name;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// since this depends on the field and field list, watch both
|
||||
$scope.$watch('config.field', getAvailableAggsForField);
|
||||
$scope.$watch('fields', getAvailableAggsForField);
|
||||
|
||||
$scope.$watch('config.agg', function (aggName) {
|
||||
var agg = Aggs.aggsByName[aggName];
|
||||
var controlsHtml = '';
|
||||
|
||||
if (agg) {
|
||||
var params = $scope.aggParams = agg.params;
|
||||
|
||||
_.forOwn(params, function (param, name) {
|
||||
// if the param doesn't have options, or a default value, skip it
|
||||
if (!param.options) return;
|
||||
// if there isn't currently a value, or the current value is not one of the options, set it to the default
|
||||
if (!$scope.config[name] || !_.find(param.options, { val: $scope.config[name] })) {
|
||||
$scope.config[name] = param.default;
|
||||
}
|
||||
});
|
||||
|
||||
if (params.order && params.size) {
|
||||
controlsHtml += ' ' + controlTemplates.orderAndSize;
|
||||
}
|
||||
|
||||
if (params.interval) {
|
||||
controlsHtml += ' ' + controlTemplates.interval;
|
||||
if (!controlsHtml.match(/aggParams\.interval\.options/)) debugger;
|
||||
}
|
||||
|
||||
if ($scope.config.categoryName === 'group') {
|
||||
controlsHtml += ' ' + controlTemplates.globalLocal;
|
||||
}
|
||||
}
|
||||
|
||||
$controls.html($compile(controlsHtml)($scope));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
config: '=',
|
||||
fields: '=',
|
||||
vis: '='
|
||||
},
|
||||
link: function ($scope, $el, attr) {
|
||||
var categoryName = $scope.config.categoryName;
|
||||
var opts = categoryOptions[categoryName];
|
||||
|
||||
$scope.Aggs = Aggs;
|
||||
$scope.Vis = Vis;
|
||||
|
||||
// attach a copy of the template to the scope and render
|
||||
$el.html($compile(opts.template)($scope));
|
||||
|
||||
_.defaults($scope.val, opts.defVal || {});
|
||||
if (opts.setup) opts.setup($scope, $el);
|
||||
|
||||
// rather than accessing vis.{{categoryName}} everywhere
|
||||
$scope[categoryName] = $scope.vis[categoryName];
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
31
src/kibana/apps/visualize/directives/visualization.js
Normal file
31
src/kibana/apps/visualize/directives/visualization.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
define(function (require) {
|
||||
var converters = require('../resp_converters/index');
|
||||
// var K4D3 = require('K4D3');
|
||||
|
||||
function VisualizationDirective() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: '<div class="chart"><pre>{{ results | json }}</pre></div>',
|
||||
scope: {
|
||||
vis: '='
|
||||
},
|
||||
link: function ($scope, $el) {
|
||||
var vis = $scope.vis;
|
||||
|
||||
vis
|
||||
.dataSource
|
||||
.on('results', function (resp) {
|
||||
$scope.results = vis.buildChartDataFromResponse(resp).groups;
|
||||
});
|
||||
|
||||
if (!vis.dataSource._$scope) {
|
||||
// only link if the dataSource isn't already linked
|
||||
vis.dataSource.$scope($scope);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
require('modules').get('kibana/directive')
|
||||
.directive('visualization', VisualizationDirective);
|
||||
});
|
306
src/kibana/apps/visualize/factories/vis.js
Normal file
306
src/kibana/apps/visualize/factories/vis.js
Normal file
|
@ -0,0 +1,306 @@
|
|||
define(function (require) {
|
||||
var converters = require('../resp_converters/index');
|
||||
var _ = require('lodash');
|
||||
|
||||
require('../services/aggs');
|
||||
|
||||
function VisFactory(Aggs, $rootScope, $q, createNotifier) {
|
||||
var notify = createNotifier({
|
||||
location: 'Visualization'
|
||||
});
|
||||
|
||||
function Vis(config, state) {
|
||||
config = config || {};
|
||||
|
||||
// the visualization type
|
||||
this.type = config.type || 'histogram';
|
||||
|
||||
// the dataSource that will populate the
|
||||
this.dataSource = $rootScope.rootDataSource.extend().size(0);
|
||||
|
||||
// master list of configs, addConfig() writes here and to the list within each
|
||||
// config category, removeConfig() does the inverse
|
||||
this.configs = [];
|
||||
|
||||
// setup each config category
|
||||
Vis.configCategories.forEach(function (category) {
|
||||
var myCat = _.defaults(config[category.name] || {}, category.defaults);
|
||||
myCat.configs = [];
|
||||
this[category.name] = myCat;
|
||||
}, this);
|
||||
|
||||
if (state) {
|
||||
// restore the passed in state
|
||||
this.setState(state);
|
||||
} else {
|
||||
this._fillConfigsToMinimum();
|
||||
}
|
||||
}
|
||||
|
||||
Vis.configCategories = [
|
||||
{
|
||||
name: 'segment',
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: Infinity
|
||||
},
|
||||
configDefaults: {
|
||||
size: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'metric',
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
configDefaults: {
|
||||
agg: 'count'
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'group',
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 1
|
||||
},
|
||||
configDefaults: {
|
||||
global: true,
|
||||
size: 5
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'split',
|
||||
defaults: {
|
||||
min: 0,
|
||||
max: 2
|
||||
},
|
||||
configDefaults: {
|
||||
size: 5
|
||||
}
|
||||
}
|
||||
];
|
||||
Vis.configCategoriesByName = _.indexBy(Vis.configCategories, 'name');
|
||||
|
||||
Vis.prototype.addConfig = function (categoryName) {
|
||||
var category = Vis.configCategoriesByName[categoryName];
|
||||
var config = _.defaults({}, category.configDefaults);
|
||||
config.categoryName = category.name;
|
||||
|
||||
this.configs.push(config);
|
||||
this[category.name].configs.push(config);
|
||||
|
||||
return config;
|
||||
};
|
||||
|
||||
Vis.prototype.removeConfig = function (config) {
|
||||
if (!config) return;
|
||||
|
||||
_.pull(this.configs, config);
|
||||
_.pull(this[config.categoryName].configs, config);
|
||||
};
|
||||
|
||||
Vis.prototype.setState = function (state) {
|
||||
var vis = this;
|
||||
|
||||
vis.dataSource.getFields(function (fields) {
|
||||
vis.configs = [];
|
||||
|
||||
_.each(state, function (categoryStates, configCategoryName) {
|
||||
if (!vis[configCategoryName]) return;
|
||||
|
||||
vis[configCategoryName].configs = [];
|
||||
|
||||
categoryStates.forEach(function (configState) {
|
||||
var config = vis.addConfig(configCategoryName);
|
||||
_.assign(config, configState);
|
||||
});
|
||||
});
|
||||
|
||||
vis._fillConfigsToMinimum();
|
||||
});
|
||||
};
|
||||
|
||||
Vis.prototype._fillConfigsToMinimum = function () {
|
||||
var vis = this;
|
||||
|
||||
// satify the min count for each category
|
||||
Vis.configCategories.forEach(function (category) {
|
||||
var myCat = vis[category.name];
|
||||
if (myCat.configs.length < myCat.min) {
|
||||
_.times(myCat.min - myCat.configs.length, function () {
|
||||
vis.addConfig(category.name);
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a list of config objects, which are ready to be turned into aggregations,
|
||||
* in the order which they should be executed.
|
||||
*
|
||||
* @return {Array} - The list of config objects
|
||||
*/
|
||||
Vis.prototype.getConfig = function () {
|
||||
var cats = {
|
||||
split: [],
|
||||
global: [],
|
||||
segment: [],
|
||||
local: [],
|
||||
metric: []
|
||||
};
|
||||
|
||||
this.configs.forEach(function (config) {
|
||||
var pos = config.categoryName;
|
||||
if (pos === 'group') pos = config.global ? 'global' : 'local';
|
||||
|
||||
if (!config.field || !config.agg) return;
|
||||
|
||||
var agg = Aggs.aggsByName[config.agg];
|
||||
if (!agg || agg.name === 'count') return;
|
||||
|
||||
var params = {
|
||||
categoryName: config.categoryName,
|
||||
agg: config.agg,
|
||||
aggParams: {
|
||||
field: config.field
|
||||
}
|
||||
};
|
||||
|
||||
// ensure that all of the declared params for the agg are declared on the config
|
||||
var valid = _.every(agg.params, function (paramDef, name) {
|
||||
if (!config[name]) return;
|
||||
if (!paramDef.custom && paramDef.options && !_.find(paramDef.options, { val: config[name] })) return;
|
||||
|
||||
// copy over the param
|
||||
params.aggParams[name] = config[name];
|
||||
|
||||
// allow provide a hook to covert string values into more complex structures
|
||||
if (paramDef.toJSON) {
|
||||
params.aggParams[name] = paramDef.toJSON(params.aggParams[name]);
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (valid) cats[pos].push(params);
|
||||
});
|
||||
|
||||
return cats.split.concat(cats.global, cats.segment, cats.local, cats.metric);
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform an ES Response into data for this visualization
|
||||
* @param {object} resp The elasticsearch response
|
||||
* @return {array} An array of flattened response rows
|
||||
*/
|
||||
Vis.prototype.buildChartDataFromResponse = function (resp) {
|
||||
notify.event('convert ES response');
|
||||
|
||||
function createGroup(bucket) {
|
||||
var g = {};
|
||||
if (bucket) g.key = bucket.key;
|
||||
return g;
|
||||
}
|
||||
|
||||
function finishRow(bucket) {
|
||||
// collect the count and bail, free metric!!
|
||||
level.rows.push(row.concat(bucket.value === void 0 ? bucket.doc_count : bucket.value));
|
||||
}
|
||||
|
||||
// all aggregations will be prefixed with:
|
||||
var aggKeyPrefix = '_agg_';
|
||||
var converter = converters[this.type];
|
||||
|
||||
// as we move into the different aggs, shift configs
|
||||
var childConfigs = this.getConfig();
|
||||
var lastCol = childConfigs[childConfigs.length - 1];
|
||||
|
||||
// into stack, and then back when we leave a level
|
||||
var stack = [];
|
||||
var row = [];
|
||||
|
||||
var chartData = createGroup();
|
||||
var level = chartData;
|
||||
|
||||
(function splitAndFlatten(bucket) {
|
||||
var col = childConfigs.shift();
|
||||
// add it to the top of the stack
|
||||
stack.unshift(col);
|
||||
|
||||
_.forOwn(bucket, function (result, key) {
|
||||
// filter out the non prefixed keys
|
||||
if (key.substr(0, aggKeyPrefix.length) !== aggKeyPrefix) return;
|
||||
|
||||
if (col.categoryName === 'split') {
|
||||
var parent = level;
|
||||
result.buckets.forEach(function (bucket) {
|
||||
var group = createGroup(bucket);
|
||||
|
||||
if (parent.groups) parent.groups.push(group);
|
||||
else parent.groups = [group];
|
||||
|
||||
level = group;
|
||||
splitAndFlatten(bucket);
|
||||
if (group.rows && group.columns) {
|
||||
group.data = converter(group.columns, group.rows);
|
||||
delete group.rows;
|
||||
delete group.columns;
|
||||
}
|
||||
});
|
||||
|
||||
level = parent;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!level.columns || !level.rows) {
|
||||
// setup this level to receive records
|
||||
level.columns = [stack[0]].concat(childConfigs);
|
||||
level.rows = [];
|
||||
|
||||
// the columns might now end in a metric, but the rows will
|
||||
if (childConfigs[childConfigs.length - 1].categoryName !== 'metric') {
|
||||
level.columns.push({
|
||||
categoryName: 'metric',
|
||||
agg: 'count'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (col.categoryName === 'metric') {
|
||||
// one row per bucket
|
||||
finishRow(result);
|
||||
} else {
|
||||
// keep digging
|
||||
result.buckets.forEach(function (bucket) {
|
||||
// track this bucket's "value" in our temporary row
|
||||
row.push(bucket.key);
|
||||
|
||||
if (col === lastCol) {
|
||||
// also grab the bucket's count
|
||||
finishRow(bucket);
|
||||
} else {
|
||||
splitAndFlatten(bucket);
|
||||
}
|
||||
|
||||
row.pop();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
childConfigs.unshift(stack.shift());
|
||||
})(resp.aggregations);
|
||||
|
||||
notify.event('convert ES response', true);
|
||||
|
||||
|
||||
return chartData;
|
||||
};
|
||||
|
||||
return Vis;
|
||||
}
|
||||
|
||||
require('modules')
|
||||
.get('kibana/services')
|
||||
.factory('Vis', VisFactory);
|
||||
});
|
|
@ -1 +1,22 @@
|
|||
<h1>Visualize</h1>
|
||||
<!-- <div class="container-fluid"> -->
|
||||
<div ng-controller="Visualize">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<form ng-submit="updateDataSource()">
|
||||
<ul class="vis-config-panel">
|
||||
<li ng-repeat="category in Vis.configCategories">
|
||||
<vis-config-category
|
||||
category-name="category.name"
|
||||
vis="vis"
|
||||
fields="fields">
|
||||
</vis-config-category>
|
||||
</li>
|
||||
</ul>
|
||||
<button type="submit" class="btn btn-primary">Update</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="col-md-9 vis-canvas">
|
||||
<visualization vis-canvas vis="vis"></visualization>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,5 +1,9 @@
|
|||
define(function (require) {
|
||||
require('css!./styles/main.css');
|
||||
|
||||
var app = require('modules').get('app/visualize');
|
||||
require('./controllers/visualize');
|
||||
|
||||
require('./directives/config_category');
|
||||
require('./directives/canvas');
|
||||
require('./directives/visualization');
|
||||
});
|
22
src/kibana/apps/visualize/partials/config_category.html
Normal file
22
src/kibana/apps/visualize/partials/config_category.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
<div
|
||||
class="panel panel-default"
|
||||
ng-if="categoryName !== 'group' || vis.segment.configs.length > 0">
|
||||
<div class="panel-heading" >
|
||||
{{ category.label }}
|
||||
<button
|
||||
ng-if="category.configs.length < category.max"
|
||||
ng-click="vis.addConfig(categoryName)"
|
||||
class="btn btn-default" >
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div ng-repeat="config in category.configs">
|
||||
<vis-config-editor
|
||||
config="config"
|
||||
vis="vis"
|
||||
fields="fields">
|
||||
</vis-config-editor>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,22 @@
|
|||
<div class="btn-group pull-right">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
ng-model="config.global"
|
||||
tooltip="Use the {{group.label}} values for every {{vis.segment.label}} value"
|
||||
tooltip-position="top"
|
||||
tooltip-append-to-body="true"
|
||||
btn-radio="true">
|
||||
Global
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
tooltip="Break up each {{vis.segment.label}} value by {{group.label}}"
|
||||
tooltip-position="top"
|
||||
tooltip-append-to-body="true"
|
||||
ng-model="config.global"
|
||||
btn-radio="false">
|
||||
Local
|
||||
</button>
|
||||
</div>
|
23
src/kibana/apps/visualize/partials/controls/interval.html
Normal file
23
src/kibana/apps/visualize/partials/controls/interval.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<table class="agg-config-interval">
|
||||
<tr>
|
||||
<td>
|
||||
<select
|
||||
ng-model="config.interval"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.interval.options"
|
||||
class="form-control"
|
||||
name="interval">
|
||||
</select>
|
||||
</td>
|
||||
<!-- <td ng-if="config.interval === 'customInterval'">
|
||||
<input
|
||||
ng-model="config.customInterval"
|
||||
class="form-control"
|
||||
type="number"
|
||||
name="custom interval">
|
||||
</td>
|
||||
<td ng-if="config.interval === 'customInterval'">
|
||||
<kbn-info info="'interval bewteen timestamp measurements, in milliseconds'"></kbn-info>
|
||||
</td> -->
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
|
@ -0,0 +1,13 @@
|
|||
<div class="row">
|
||||
<div class="col-sm-6">
|
||||
<select
|
||||
class="form-control"
|
||||
name="order"
|
||||
ng-model="config.order"
|
||||
ng-options="opt.val as opt.display for opt in aggParams.order.options">
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="number" ng-model="config.size" name="size">
|
||||
</div>
|
||||
</div>
|
19
src/kibana/apps/visualize/partials/editor/dimension.html
Normal file
19
src/kibana/apps/visualize/partials/editor/dimension.html
Normal file
|
@ -0,0 +1,19 @@
|
|||
<div class="form-group">
|
||||
<label for="field">
|
||||
Field
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="field"
|
||||
ng-model="config.field"
|
||||
ng-options="field.name as field.name group by field.type for field in fields">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-show="availableAggs">
|
||||
<select
|
||||
name="agg"
|
||||
ng-model="config.agg"
|
||||
ng-options="agg.name as agg.display for agg in availableAggs">
|
||||
</select>
|
||||
</div>
|
||||
<div class="agg-param-controls">
|
21
src/kibana/apps/visualize/partials/editor/metric.html
Normal file
21
src/kibana/apps/visualize/partials/editor/metric.html
Normal file
|
@ -0,0 +1,21 @@
|
|||
<div class="form-group">
|
||||
<label for="stat">Stat</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="stat"
|
||||
ng-model="config.agg"
|
||||
ng-options="agg.name as agg.name for agg in Aggs.metricAggs">
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="config.agg && config.agg !== 'count'">
|
||||
<label for="field">
|
||||
Field to {{config.agg}}
|
||||
<kbn-info placement="right" info="'Field to use for the ' + metric.label"></kbn-info>
|
||||
</label>
|
||||
<select
|
||||
class="form-control"
|
||||
name="field"
|
||||
ng-model="config.field"
|
||||
ng-options="field.name as field.name for field in fields | fieldType:'number'">
|
||||
</select>
|
||||
</div>
|
4
src/kibana/apps/visualize/resp_converters/flatten.js
Normal file
4
src/kibana/apps/visualize/resp_converters/flatten.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
define(function () {
|
||||
var _ = require('lodash');
|
||||
var aggKeyPrefix = '_agg_';
|
||||
})
|
35
src/kibana/apps/visualize/resp_converters/histogram.js
Normal file
35
src/kibana/apps/visualize/resp_converters/histogram.js
Normal file
|
@ -0,0 +1,35 @@
|
|||
define(function (require) {
|
||||
var _ = require('lodash');
|
||||
|
||||
return function (columns, rows) {
|
||||
var serieses = [];
|
||||
var seriesesByLabel = {};
|
||||
|
||||
// index of color
|
||||
var iColor = _.findIndex(columns, { categoryName: 'group' });
|
||||
// index of x-axis
|
||||
var iX = _.findIndex(columns, { categoryName: 'segment'});
|
||||
// index of y-axis
|
||||
var iY = _.findIndex(columns, { categoryName: 'metric'});
|
||||
|
||||
rows.forEach(function (row) {
|
||||
var series = seriesesByLabel[iColor === -1 ? 'undefined' : row[iColor]];
|
||||
|
||||
if (!series) {
|
||||
series = {
|
||||
label: '' + row[iColor],
|
||||
children: []
|
||||
};
|
||||
serieses.push(series);
|
||||
seriesesByLabel[series.label] = series;
|
||||
}
|
||||
|
||||
series.children.push([
|
||||
row[iX], // x-axis value
|
||||
row[iY === -1 ? row.length - 1 : iY] // y-axis value
|
||||
]);
|
||||
});
|
||||
|
||||
return serieses;
|
||||
};
|
||||
});
|
5
src/kibana/apps/visualize/resp_converters/index.js
Normal file
5
src/kibana/apps/visualize/resp_converters/index.js
Normal file
|
@ -0,0 +1,5 @@
|
|||
define(function (require) {
|
||||
return {
|
||||
histogram: require('./histogram')
|
||||
};
|
||||
});
|
120
src/kibana/apps/visualize/services/aggs.js
Normal file
120
src/kibana/apps/visualize/services/aggs.js
Normal file
|
@ -0,0 +1,120 @@
|
|||
define(function (require) {
|
||||
|
||||
require('utils/mixins');
|
||||
var _ = require('lodash');
|
||||
|
||||
function AggsService() {
|
||||
this.metricAggs = [
|
||||
{
|
||||
name: 'count',
|
||||
display: 'Count'
|
||||
},
|
||||
{
|
||||
name: 'avg',
|
||||
display: 'Average'
|
||||
},
|
||||
{
|
||||
name: 'sum',
|
||||
display: 'Sum'
|
||||
},
|
||||
{
|
||||
name: 'min',
|
||||
display: 'Min'
|
||||
},
|
||||
{
|
||||
name: 'max',
|
||||
display: 'Max'
|
||||
}
|
||||
];
|
||||
this.metricAggsByName = _.indexBy(this.metricAggs, 'name');
|
||||
|
||||
this.bucketAggs = [
|
||||
{
|
||||
name: 'histogram',
|
||||
display: 'Histogram',
|
||||
params: {
|
||||
size: {},
|
||||
order: {
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
default: 'desc',
|
||||
toJSON: function (val) {
|
||||
return { _count: val };
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'terms',
|
||||
display: 'Terms',
|
||||
params: {
|
||||
size: {},
|
||||
order: {
|
||||
options: [
|
||||
{ display: 'Top', val: 'desc' },
|
||||
{ display: 'Bottom', val: 'asc' }
|
||||
],
|
||||
default: 'desc',
|
||||
toJSON: function (val) {
|
||||
return { _count: val };
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'date_histogram',
|
||||
display: 'Date Histogram',
|
||||
params: {
|
||||
interval: {
|
||||
options: [
|
||||
{ display: 'Hourly', val: 'hour' },
|
||||
{ display: 'Daily', val: 'day' },
|
||||
{ display: 'Weekly', val: 'week' },
|
||||
{ display: 'Monthly', val: 'month' },
|
||||
{ display: 'Quarterly', val: 'quarter' },
|
||||
{ display: 'Yearly', val: 'year' }
|
||||
],
|
||||
default: 'hour'
|
||||
},
|
||||
}
|
||||
}
|
||||
];
|
||||
this.bucketAggsByName = _.indexBy(this.bucketAggs, 'name');
|
||||
|
||||
this.aggsByName = _.assign({}, this.bucketAggsByName, this.metricAggsByName);
|
||||
|
||||
this.aggsByFieldType = {
|
||||
number: [
|
||||
this.bucketAggsByName.histogram,
|
||||
this.bucketAggsByName.terms,
|
||||
// 'range'
|
||||
],
|
||||
date: [
|
||||
// 'date range',
|
||||
this.bucketAggsByName.date_histogram,
|
||||
],
|
||||
boolean: [
|
||||
// 'terms'
|
||||
],
|
||||
ip: [
|
||||
// 'ipv4 range'
|
||||
],
|
||||
geo_point: [
|
||||
// 'geo distance'
|
||||
],
|
||||
geo_shape: [
|
||||
// 'geohash grid'
|
||||
],
|
||||
string: [
|
||||
// 'significant terms',
|
||||
this.bucketAggsByName.terms,
|
||||
// 'range'
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
require('modules').get('app/visualize')
|
||||
.service('Aggs', AggsService);
|
||||
});
|
17
src/kibana/apps/visualize/services/visualizations.js
Normal file
17
src/kibana/apps/visualize/services/visualizations.js
Normal file
|
@ -0,0 +1,17 @@
|
|||
define(function (require) {
|
||||
var app = require('modules').get('app/visualize');
|
||||
|
||||
function VisualizationsService(es, courier, $q, $timeout) {
|
||||
this.get = function (id) {
|
||||
var defer = $q.defer();
|
||||
|
||||
$timeout(function () {
|
||||
defer.reject('not implemented');
|
||||
}, 2000);
|
||||
|
||||
return defer.promise();
|
||||
};
|
||||
}
|
||||
|
||||
app.service('Visualizations', VisualizationsService);
|
||||
});
|
|
@ -0,0 +1,32 @@
|
|||
.vis-config-panel {
|
||||
padding: 0;
|
||||
}
|
||||
.vis-config-panel > li {
|
||||
list-style: none;
|
||||
}
|
||||
.vis-config-panel > li .panel-heading {
|
||||
position: relative;
|
||||
}
|
||||
.vis-config-panel > li .panel-heading button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 7px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
.vis-config-panel .agg-config-interval td {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.vis-config-panel .agg-config-interval td:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
.vis-canvas {
|
||||
padding: 15px 0;
|
||||
}
|
||||
.vis-canvas visualization {
|
||||
display: block;
|
||||
background: black;
|
||||
height: 800px;
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
.vis-config-panel {
|
||||
padding: 0;
|
||||
|
||||
> li {
|
||||
list-style: none;
|
||||
|
||||
.panel-heading {
|
||||
position: relative;
|
||||
|
||||
button {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 7px;
|
||||
padding: 3px 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.agg-config-interval {
|
||||
td {
|
||||
padding-left: 10px;
|
||||
&:first-child {
|
||||
padding-left: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.vis-canvas {
|
||||
padding: 15px 0;
|
||||
|
||||
visualization {
|
||||
display: block;
|
||||
background: black;
|
||||
height: 800px;
|
||||
width: 95%;
|
||||
margin: 0 auto;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ define(function (require) {
|
|||
|
||||
require('services/config');
|
||||
require('services/courier');
|
||||
require('directives/info');
|
||||
require('angular-bootstrap');
|
||||
|
||||
require('modules')
|
||||
|
@ -30,7 +31,7 @@ define(function (require) {
|
|||
});
|
||||
|
||||
$rootScope.rootDataSource = courier.createSource('search')
|
||||
.index('_all');
|
||||
.index('logstash-*');
|
||||
|
||||
$scope.opts = {
|
||||
activeFetchInterval: void 0,
|
||||
|
|
|
@ -7,6 +7,7 @@ define(function (require) {
|
|||
var setTO = setTimeout;
|
||||
var clearTO = clearTimeout;
|
||||
var log = (typeof KIBANA_DIST === 'undefined') ? _.bindKey(console, 'log') : _.noop;
|
||||
var consoleGroups = ('group' in window.console) && ('groupCollapsed' in window.console) && ('groupEnd' in window.console);
|
||||
|
||||
var fatalToastTemplate = (function lazyTemplate(tmpl) {
|
||||
var compiled;
|
||||
|
@ -74,14 +75,6 @@ define(function (require) {
|
|||
return msg + '\n' + stack.map(function (line) { return ' ' + line; }).join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Track application lifecycle events
|
||||
* @type {[type]}
|
||||
*/
|
||||
var lifecycleEvents = window.kibanaLifecycleEvents = {};
|
||||
|
||||
var applicationBooted;
|
||||
|
||||
/**
|
||||
* Functionality to check that
|
||||
*/
|
||||
|
@ -95,26 +88,65 @@ define(function (require) {
|
|||
this._notifs = notifs;
|
||||
}
|
||||
|
||||
NotifyManager.prototype.lifecycle = function (name, success) {
|
||||
var status;
|
||||
if (name === 'bootstrap' && success === true) applicationBooted = true;
|
||||
NotifyManager.prototype.log = log;
|
||||
|
||||
if (success === void 0) {
|
||||
// start
|
||||
lifecycleEvents[name] = now();
|
||||
} else {
|
||||
// end
|
||||
if (success) {
|
||||
lifecycleEvents[name] = now() - (lifecycleEvents[name] || 0);
|
||||
status = lifecycleEvents[name].toFixed(2) + ' ms';
|
||||
// general functionality used by .event() and .lifecycle()
|
||||
function createGroupLogger(type, opts) {
|
||||
// Track the groups managed by this logger
|
||||
var groups = window[type + 'Groups'] = {};
|
||||
|
||||
return function (name, success) {
|
||||
var status;
|
||||
if (success === void 0) {
|
||||
// start
|
||||
groups[name] = now();
|
||||
} else {
|
||||
lifecycleEvents[name] = false;
|
||||
status = 'failure';
|
||||
}
|
||||
}
|
||||
groups[name] = now() - (groups[name] || 0);
|
||||
var time = ' in ' + groups[name].toFixed(2) + 'ms';
|
||||
|
||||
log('KBN: ' + name + (status ? ' - ' + status : ''));
|
||||
};
|
||||
// end
|
||||
if (success) {
|
||||
status = 'complete' + time;
|
||||
} else {
|
||||
groups[name] = false;
|
||||
status = 'failure' + time;
|
||||
}
|
||||
}
|
||||
|
||||
if (consoleGroups) {
|
||||
if (status) {
|
||||
console.log(status);
|
||||
console.groupEnd();
|
||||
} else {
|
||||
if (opts.open) {
|
||||
console.group(name);
|
||||
} else {
|
||||
console.groupCollapsed(name);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log('KBN: ' + name + (status ? ' - ' + status : ''));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a sometimes redundant event
|
||||
* @param {string} name - The name of the group
|
||||
* @param {boolean} success - Simple flag stating whether the event succeeded
|
||||
*/
|
||||
NotifyManager.prototype.event = createGroupLogger('event', {
|
||||
open: false
|
||||
});
|
||||
|
||||
/**
|
||||
* Log a major, important, event in the lifecycle of the application
|
||||
* @param {string} name - The name of the lifecycle event
|
||||
* @param {boolean} success - Simple flag stating whether the lifecycle event succeeded
|
||||
*/
|
||||
NotifyManager.prototype.lifecycle = createGroupLogger('lifecycle', {
|
||||
open: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Kill the page, and display an error
|
||||
|
|
|
@ -144,7 +144,7 @@ define(function (require) {
|
|||
*******/
|
||||
|
||||
function _change(key, val) {
|
||||
notify.lifecycle('config change: ' + key + ': ' + vals[key] + ' -> ' + val);
|
||||
notify.log('config change: ' + key + ': ' + vals[key] + ' -> ' + val);
|
||||
triggerWatchers(watchers[key], val, vals[key]);
|
||||
vals[key] = val;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue