From 38d91151953e4cd850c078952e91ad57ebfb3eb7 Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Fri, 21 Mar 2014 16:05:58 -0700 Subject: [PATCH] Visualize skeleton in place --- .../apps/visualize/controllers/visualize.js | 140 ++++++++ .../apps/visualize/directives/canvas.js | 25 ++ .../visualize/directives/config_category.js | 22 ++ .../visualize/directives/config_controlls.js | 58 ++++ .../visualize/directives/config_editor.js | 133 ++++++++ .../visualize/directives/visualization.js | 31 ++ src/kibana/apps/visualize/factories/vis.js | 306 ++++++++++++++++++ src/kibana/apps/visualize/index.html | 23 +- src/kibana/apps/visualize/index.js | 6 +- .../visualize/partials/config_category.html | 22 ++ .../partials/controls/global_local.html | 22 ++ .../visualize/partials/controls/interval.html | 23 ++ .../partials/controls/order_and_size.html | 13 + .../visualize/partials/editor/dimension.html | 19 ++ .../visualize/partials/editor/metric.html | 21 ++ .../apps/visualize/resp_converters/flatten.js | 4 + .../visualize/resp_converters/histogram.js | 35 ++ .../apps/visualize/resp_converters/index.js | 5 + src/kibana/apps/visualize/services/aggs.js | 120 +++++++ .../apps/visualize/services/visualizations.js | 17 + src/kibana/apps/visualize/styles/main.css | 32 ++ src/kibana/apps/visualize/styles/main.html | 0 src/kibana/apps/visualize/styles/main.less | 40 +++ src/kibana/controllers/kibana.js | 3 +- src/kibana/notify/manager.js | 82 +++-- src/kibana/services/config.js | 2 +- 26 files changed, 1175 insertions(+), 29 deletions(-) create mode 100644 src/kibana/apps/visualize/controllers/visualize.js create mode 100644 src/kibana/apps/visualize/directives/canvas.js create mode 100644 src/kibana/apps/visualize/directives/config_category.js create mode 100644 src/kibana/apps/visualize/directives/config_controlls.js create mode 100644 src/kibana/apps/visualize/directives/config_editor.js create mode 100644 src/kibana/apps/visualize/directives/visualization.js create mode 100644 src/kibana/apps/visualize/factories/vis.js create mode 100644 src/kibana/apps/visualize/partials/config_category.html create mode 100644 src/kibana/apps/visualize/partials/controls/global_local.html create mode 100644 src/kibana/apps/visualize/partials/controls/interval.html create mode 100644 src/kibana/apps/visualize/partials/controls/order_and_size.html create mode 100644 src/kibana/apps/visualize/partials/editor/dimension.html create mode 100644 src/kibana/apps/visualize/partials/editor/metric.html create mode 100644 src/kibana/apps/visualize/resp_converters/flatten.js create mode 100644 src/kibana/apps/visualize/resp_converters/histogram.js create mode 100644 src/kibana/apps/visualize/resp_converters/index.js create mode 100644 src/kibana/apps/visualize/services/aggs.js create mode 100644 src/kibana/apps/visualize/services/visualizations.js delete mode 100644 src/kibana/apps/visualize/styles/main.html diff --git a/src/kibana/apps/visualize/controllers/visualize.js b/src/kibana/apps/visualize/controllers/visualize.js new file mode 100644 index 00000000000000..70588d921fdae0 --- /dev/null +++ b/src/kibana/apps/visualize/controllers/visualize.js @@ -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); + }; + + }); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/directives/canvas.js b/src/kibana/apps/visualize/directives/canvas.js new file mode 100644 index 00000000000000..dce77da9fe2c86 --- /dev/null +++ b/src/kibana/apps/visualize/directives/canvas.js @@ -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); + }); + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/directives/config_category.js b/src/kibana/apps/visualize/directives/config_category.js new file mode 100644 index 00000000000000..ace93cf7ccc553 --- /dev/null +++ b/src/kibana/apps/visualize/directives/config_category.js @@ -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]; + } + }; + }); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/directives/config_controlls.js b/src/kibana/apps/visualize/directives/config_controlls.js new file mode 100644 index 00000000000000..88458190af1102 --- /dev/null +++ b/src/kibana/apps/visualize/directives/config_controlls.js @@ -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; + } + }; + }); + +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/directives/config_editor.js b/src/kibana/apps/visualize/directives/config_editor.js new file mode 100644 index 00000000000000..fc37e097f1e1f5 --- /dev/null +++ b/src/kibana/apps/visualize/directives/config_editor.js @@ -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]; + } + }; + }); + +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/directives/visualization.js b/src/kibana/apps/visualize/directives/visualization.js new file mode 100644 index 00000000000000..b080eb5ee2a4b0 --- /dev/null +++ b/src/kibana/apps/visualize/directives/visualization.js @@ -0,0 +1,31 @@ +define(function (require) { + var converters = require('../resp_converters/index'); + // var K4D3 = require('K4D3'); + + function VisualizationDirective() { + return { + restrict: 'E', + template: '
{{ results | json }}
', + 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); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/factories/vis.js b/src/kibana/apps/visualize/factories/vis.js new file mode 100644 index 00000000000000..3bc79127ce1e36 --- /dev/null +++ b/src/kibana/apps/visualize/factories/vis.js @@ -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); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/index.html b/src/kibana/apps/visualize/index.html index 9f5d89e1c5b6f6..b2a129a21220ec 100644 --- a/src/kibana/apps/visualize/index.html +++ b/src/kibana/apps/visualize/index.html @@ -1 +1,22 @@ -

Visualize

\ No newline at end of file + +
+
+
+
+
    +
  • + + +
  • +
+ +
+
+
+ +
+
+
\ No newline at end of file diff --git a/src/kibana/apps/visualize/index.js b/src/kibana/apps/visualize/index.js index 4de8f308fc7411..0bd9e161e3f1c1 100644 --- a/src/kibana/apps/visualize/index.js +++ b/src/kibana/apps/visualize/index.js @@ -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'); }); \ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/config_category.html b/src/kibana/apps/visualize/partials/config_category.html new file mode 100644 index 00000000000000..e441eece215da1 --- /dev/null +++ b/src/kibana/apps/visualize/partials/config_category.html @@ -0,0 +1,22 @@ +
+
+ {{ category.label }} + +
+
+
+ + +
+
+
\ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/controls/global_local.html b/src/kibana/apps/visualize/partials/controls/global_local.html new file mode 100644 index 00000000000000..eb18d744a03338 --- /dev/null +++ b/src/kibana/apps/visualize/partials/controls/global_local.html @@ -0,0 +1,22 @@ +
+ + +
\ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/controls/interval.html b/src/kibana/apps/visualize/partials/controls/interval.html new file mode 100644 index 00000000000000..d40275818d1f3a --- /dev/null +++ b/src/kibana/apps/visualize/partials/controls/interval.html @@ -0,0 +1,23 @@ + + + + + +
+ +
+ \ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/controls/order_and_size.html b/src/kibana/apps/visualize/partials/controls/order_and_size.html new file mode 100644 index 00000000000000..71d5e7c16d2154 --- /dev/null +++ b/src/kibana/apps/visualize/partials/controls/order_and_size.html @@ -0,0 +1,13 @@ +
+
+ +
+
+ +
+
\ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/editor/dimension.html b/src/kibana/apps/visualize/partials/editor/dimension.html new file mode 100644 index 00000000000000..edff5030c83320 --- /dev/null +++ b/src/kibana/apps/visualize/partials/editor/dimension.html @@ -0,0 +1,19 @@ +
+ + +
+
+ +
+
\ No newline at end of file diff --git a/src/kibana/apps/visualize/partials/editor/metric.html b/src/kibana/apps/visualize/partials/editor/metric.html new file mode 100644 index 00000000000000..e616a88837b4ea --- /dev/null +++ b/src/kibana/apps/visualize/partials/editor/metric.html @@ -0,0 +1,21 @@ +
+ + +
+
+ + +
\ No newline at end of file diff --git a/src/kibana/apps/visualize/resp_converters/flatten.js b/src/kibana/apps/visualize/resp_converters/flatten.js new file mode 100644 index 00000000000000..087a0eba6c2d9e --- /dev/null +++ b/src/kibana/apps/visualize/resp_converters/flatten.js @@ -0,0 +1,4 @@ +define(function () { + var _ = require('lodash'); + var aggKeyPrefix = '_agg_'; +}) \ No newline at end of file diff --git a/src/kibana/apps/visualize/resp_converters/histogram.js b/src/kibana/apps/visualize/resp_converters/histogram.js new file mode 100644 index 00000000000000..4a773605544230 --- /dev/null +++ b/src/kibana/apps/visualize/resp_converters/histogram.js @@ -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; + }; +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/resp_converters/index.js b/src/kibana/apps/visualize/resp_converters/index.js new file mode 100644 index 00000000000000..4e20177cefe81f --- /dev/null +++ b/src/kibana/apps/visualize/resp_converters/index.js @@ -0,0 +1,5 @@ +define(function (require) { + return { + histogram: require('./histogram') + }; +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/services/aggs.js b/src/kibana/apps/visualize/services/aggs.js new file mode 100644 index 00000000000000..36e010ad1a099e --- /dev/null +++ b/src/kibana/apps/visualize/services/aggs.js @@ -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); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/services/visualizations.js b/src/kibana/apps/visualize/services/visualizations.js new file mode 100644 index 00000000000000..64561485925e42 --- /dev/null +++ b/src/kibana/apps/visualize/services/visualizations.js @@ -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); +}); \ No newline at end of file diff --git a/src/kibana/apps/visualize/styles/main.css b/src/kibana/apps/visualize/styles/main.css index e69de29bb2d1d6..6dfa2c9b9e66e6 100644 --- a/src/kibana/apps/visualize/styles/main.css +++ b/src/kibana/apps/visualize/styles/main.css @@ -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; +} diff --git a/src/kibana/apps/visualize/styles/main.html b/src/kibana/apps/visualize/styles/main.html deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/src/kibana/apps/visualize/styles/main.less b/src/kibana/apps/visualize/styles/main.less index e69de29bb2d1d6..87e50c50670e26 100644 --- a/src/kibana/apps/visualize/styles/main.less +++ b/src/kibana/apps/visualize/styles/main.less @@ -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; + } +} \ No newline at end of file diff --git a/src/kibana/controllers/kibana.js b/src/kibana/controllers/kibana.js index 7b2782c43e59e2..05ac874090fb0d 100644 --- a/src/kibana/controllers/kibana.js +++ b/src/kibana/controllers/kibana.js @@ -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, diff --git a/src/kibana/notify/manager.js b/src/kibana/notify/manager.js index c385cb9d05a6c4..256630b4851279 100644 --- a/src/kibana/notify/manager.js +++ b/src/kibana/notify/manager.js @@ -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; - - if (success === void 0) { - // start - lifecycleEvents[name] = now(); - } else { - // end - if (success) { - lifecycleEvents[name] = now() - (lifecycleEvents[name] || 0); - status = lifecycleEvents[name].toFixed(2) + ' ms'; + NotifyManager.prototype.log = log; + + // 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'; + + // end + if (success) { + status = 'complete' + time; + } else { + groups[name] = false; + status = 'failure' + time; + } } - } - log('KBN: ' + name + (status ? ' - ' + status : '')); - }; + 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 diff --git a/src/kibana/services/config.js b/src/kibana/services/config.js index 758aca1067e234..abb372dac11717 100644 --- a/src/kibana/services/config.js +++ b/src/kibana/services/config.js @@ -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; }