Merged Pull Request #484
This commit is contained in:
Executable
+137
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* main app level module
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'require',
|
||||
|
||||
'elasticjs',
|
||||
'bootstrap',
|
||||
'angular-sanitize',
|
||||
'angular-strap'
|
||||
],
|
||||
function (angular, $, _, appLevelRequire) {
|
||||
"use strict";
|
||||
|
||||
var app = angular.module('kibana', []),
|
||||
// we will keep a reference to each module defined before boot, so that we can
|
||||
// go back and allow it to define new features later. Once we boot, this will be false
|
||||
pre_boot_modules = [],
|
||||
// these are the functions that we need to call to register different
|
||||
// features if we define them after boot time
|
||||
register_fns = {};
|
||||
|
||||
/**
|
||||
* Tells the application to watch the module, once bootstraping has completed
|
||||
* the modules controller, service, etc. functions will be overwritten to register directly
|
||||
* with this application.
|
||||
* @param {[type]} module [description]
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
app.useModule = function (module) {
|
||||
if (pre_boot_modules) {
|
||||
pre_boot_modules.push(module);
|
||||
} else {
|
||||
_.extend(module, register_fns);
|
||||
}
|
||||
return module;
|
||||
};
|
||||
|
||||
app.safeApply = function ($scope, fn) {
|
||||
switch($scope.$$phase) {
|
||||
case '$apply':
|
||||
// $digest hasn't started, we should be good
|
||||
$scope.$eval(fn);
|
||||
break;
|
||||
case '$digest':
|
||||
// waiting to $apply the changes
|
||||
setTimeout(function () { app.safeApply($scope, fn); }, 10);
|
||||
break;
|
||||
default:
|
||||
// clear to begin an $apply $$phase
|
||||
$scope.$apply(fn);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
app.config(function ($routeProvider, $controllerProvider, $compileProvider, $filterProvider, $provide) {
|
||||
$routeProvider
|
||||
.when('/dashboard', {
|
||||
templateUrl: 'app/partials/dashboard.html',
|
||||
})
|
||||
.when('/dashboard/:kbnType/:kbnId', {
|
||||
templateUrl: 'app/partials/dashboard.html',
|
||||
})
|
||||
.when('/dashboard/:kbnType/:kbnId/:params', {
|
||||
templateUrl: 'app/partials/dashboard.html'
|
||||
})
|
||||
.otherwise({
|
||||
redirectTo: 'dashboard'
|
||||
});
|
||||
// this is how the internet told me to dynamically add modules :/
|
||||
register_fns.controller = $controllerProvider.register;
|
||||
register_fns.directive = $compileProvider.directive;
|
||||
register_fns.factory = $provide.factory;
|
||||
register_fns.service = $provide.service;
|
||||
register_fns.filter = $filterProvider.register;
|
||||
});
|
||||
|
||||
var apps_deps = [
|
||||
'elasticjs.service',
|
||||
'$strap.directives',
|
||||
'ngSanitize',
|
||||
'kibana',
|
||||
];
|
||||
|
||||
_.each('controllers directives factories services filters'.split(' '),
|
||||
function (type) {
|
||||
var module_name = 'kibana.'+type;
|
||||
// create the module
|
||||
app.useModule(angular.module(module_name, []));
|
||||
// push it into the apps dependencies
|
||||
apps_deps.push(module_name);
|
||||
});
|
||||
|
||||
app.panel_helpers = {
|
||||
partial: function (name) {
|
||||
return 'app/partials/'+name+'.html';
|
||||
}
|
||||
};
|
||||
|
||||
// load the core components
|
||||
require([
|
||||
'controllers/all',
|
||||
'directives/all',
|
||||
'filters/all'
|
||||
], function () {
|
||||
|
||||
// bootstrap the app
|
||||
angular
|
||||
.element(document)
|
||||
.ready(function() {
|
||||
$('body').attr('ng-controller', 'DashCtrl');
|
||||
angular.bootstrap(document, apps_deps)
|
||||
.invoke(['$rootScope', function ($rootScope) {
|
||||
_.each(pre_boot_modules, function (module) {
|
||||
_.extend(module, register_fns);
|
||||
});
|
||||
pre_boot_modules = false;
|
||||
|
||||
$rootScope.requireContext = appLevelRequire;
|
||||
$rootScope.require = function (deps, fn) {
|
||||
var $scope = this;
|
||||
$scope.requireContext(deps, function () {
|
||||
var deps = _.toArray(arguments);
|
||||
$scope.$apply(function () {
|
||||
fn.apply($scope, deps);
|
||||
});
|
||||
});
|
||||
};
|
||||
}]);
|
||||
});
|
||||
});
|
||||
|
||||
return app;
|
||||
});
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
define(['jquery-src'],
|
||||
function ($) {
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* jQuery extensions
|
||||
*/
|
||||
var $win = $(window);
|
||||
|
||||
$.fn.place_tt = (function () {
|
||||
var defaults = {
|
||||
offset: 5,
|
||||
css: {
|
||||
position : 'absolute',
|
||||
top : -1000,
|
||||
left : 0,
|
||||
color : "#c8c8c8",
|
||||
padding : '10px',
|
||||
'font-size': '11pt',
|
||||
'font-weight' : 200,
|
||||
'background-color': '#1f1f1f',
|
||||
'border-radius': '5px',
|
||||
}
|
||||
};
|
||||
|
||||
return function (x, y, opts) {
|
||||
opts = $.extend(true, {}, defaults, opts);
|
||||
return this.each(function () {
|
||||
var $tooltip = $(this), width, height;
|
||||
|
||||
$tooltip.css(opts.css);
|
||||
if (!$.contains(document.body, $tooltip[0])) {
|
||||
$tooltip.appendTo(document.body);
|
||||
}
|
||||
|
||||
width = $tooltip.outerWidth(true);
|
||||
height = $tooltip.outerHeight(true);
|
||||
|
||||
$tooltip.css('left', x + opts.offset + width > $win.width() ? x - opts.offset - width : x + opts.offset);
|
||||
$tooltip.css('top', y + opts.offset + height > $win.height() ? y - opts.offset - height : y + opts.offset);
|
||||
});
|
||||
};
|
||||
})();
|
||||
|
||||
return $;
|
||||
});
|
||||
Executable
+301
@@ -0,0 +1,301 @@
|
||||
define(['jquery', 'underscore'],
|
||||
function($, _) {
|
||||
'use strict';
|
||||
|
||||
var kbn = {};
|
||||
|
||||
kbn.get_object_fields = function(obj) {
|
||||
var field_array = [];
|
||||
obj = kbn.flatten_json(obj._source);
|
||||
for (var field in obj) {
|
||||
field_array.push(field);
|
||||
}
|
||||
return field_array.sort();
|
||||
};
|
||||
|
||||
kbn.get_all_fields = function(data) {
|
||||
var fields = [];
|
||||
_.each(data,function(hit) {
|
||||
fields = _.uniq(fields.concat(_.keys(hit)));
|
||||
});
|
||||
// Remove stupid angular key
|
||||
fields = _.without(fields,'$$hashKey');
|
||||
return fields;
|
||||
};
|
||||
|
||||
kbn.has_field = function(obj,field) {
|
||||
var obj_fields = kbn.get_object_fields(obj);
|
||||
if (_.inArray(obj_fields,field) < 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
kbn.get_related_fields = function(docs,field) {
|
||||
var field_array = [];
|
||||
_.each(docs, function(doc) {
|
||||
var keys = _.keys(doc);
|
||||
if(_.contains(keys,field)) {
|
||||
field_array = field_array.concat(keys);
|
||||
}
|
||||
});
|
||||
var counts = _.countBy(_.without(field_array,field),function(field){return field;});
|
||||
return counts;
|
||||
};
|
||||
|
||||
kbn.recurse_field_dots = function(object,field) {
|
||||
var value = null;
|
||||
var nested;
|
||||
if (typeof object[field] !== 'undefined') {
|
||||
value = object[field];
|
||||
}
|
||||
else if (nested = field.match(/(.*?)\.(.*)/)) {
|
||||
if(typeof object[nested[1]] !== 'undefined') {
|
||||
value = (typeof object[nested[1]][nested[2]] !== 'undefined') ?
|
||||
object[nested[1]][nested[2]] : kbn.recurse_field_dots(
|
||||
object[nested[1]],nested[2]);
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
};
|
||||
|
||||
kbn.top_field_values = function(docs,field,count) {
|
||||
var all_values = _.pluck(docs,field),
|
||||
groups = {};
|
||||
|
||||
// manually grouping into pairs allows us to keep the original value,
|
||||
_.each(all_values, function (value) {
|
||||
var key = _.isUndefined(value) ? '' : value.toString();
|
||||
if (_.has(groups, key)) {
|
||||
groups[key][1] ++;
|
||||
} else {
|
||||
groups[key] = [value, 1];
|
||||
}
|
||||
});
|
||||
|
||||
return _.values(groups).sort(function(a, b) {
|
||||
return a[1] - b[1];
|
||||
}).reverse().slice(0,count);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculate a graph interval
|
||||
*
|
||||
* from:: Date object containing the start time
|
||||
* to:: Date object containing the finish time
|
||||
* size:: Calculate to approximately this many bars
|
||||
* user_interval:: User specified histogram interval
|
||||
*
|
||||
*/
|
||||
kbn.calculate_interval = function(from,to,size,user_interval) {
|
||||
if(_.isObject(from)) {
|
||||
from = from.valueOf();
|
||||
}
|
||||
if(_.isObject(to)) {
|
||||
to = to.valueOf();
|
||||
}
|
||||
return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
|
||||
};
|
||||
|
||||
kbn.round_interval = function(interval) {
|
||||
switch (true) {
|
||||
// 0.5s
|
||||
case (interval <= 500):
|
||||
return 100; // 0.1s
|
||||
// 5s
|
||||
case (interval <= 5000):
|
||||
return 1000; // 1s
|
||||
// 7.5s
|
||||
case (interval <= 7500):
|
||||
return 5000; // 5s
|
||||
// 15s
|
||||
case (interval <= 15000):
|
||||
return 10000; // 10s
|
||||
// 45s
|
||||
case (interval <= 45000):
|
||||
return 30000; // 30s
|
||||
// 3m
|
||||
case (interval <= 180000):
|
||||
return 60000; // 1m
|
||||
// 9m
|
||||
case (interval <= 450000):
|
||||
return 300000; // 5m
|
||||
// 20m
|
||||
case (interval <= 1200000):
|
||||
return 600000; // 10m
|
||||
// 45m
|
||||
case (interval <= 2700000):
|
||||
return 1800000; // 30m
|
||||
// 2h
|
||||
case (interval <= 7200000):
|
||||
return 3600000; // 1h
|
||||
// 6h
|
||||
case (interval <= 21600000):
|
||||
return 10800000; // 3h
|
||||
// 24h
|
||||
case (interval <= 86400000):
|
||||
return 43200000; // 12h
|
||||
// 48h
|
||||
case (interval <= 172800000):
|
||||
return 86400000; // 24h
|
||||
// 1w
|
||||
case (interval <= 604800000):
|
||||
return 86400000; // 24h
|
||||
// 3w
|
||||
case (interval <= 1814400000):
|
||||
return 604800000; // 1w
|
||||
// 2y
|
||||
case (interval < 3628800000):
|
||||
return 2592000000; // 30d
|
||||
default:
|
||||
return 31536000000; // 1y
|
||||
}
|
||||
};
|
||||
|
||||
kbn.secondsToHms = function(seconds){
|
||||
var numyears = Math.floor(seconds / 31536000);
|
||||
if(numyears){
|
||||
return numyears + 'y';
|
||||
}
|
||||
var numdays = Math.floor((seconds % 31536000) / 86400);
|
||||
if(numdays){
|
||||
return numdays + 'd';
|
||||
}
|
||||
var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
|
||||
if(numhours){
|
||||
return numhours + 'h';
|
||||
}
|
||||
var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
|
||||
if(numminutes){
|
||||
return numminutes + 'm';
|
||||
}
|
||||
var numseconds = (((seconds % 31536000) % 86400) % 3600) % 60;
|
||||
if(numseconds){
|
||||
return numseconds + 's';
|
||||
}
|
||||
return 'less then a second'; //'just now' //or other string you like;
|
||||
};
|
||||
|
||||
kbn.to_percent = function(number,outof) {
|
||||
return Math.floor((number/outof)*10000)/100 + "%";
|
||||
};
|
||||
|
||||
kbn.addslashes = function(str) {
|
||||
str = str.replace(/\\/g, '\\\\');
|
||||
str = str.replace(/\'/g, '\\\'');
|
||||
str = str.replace(/\"/g, '\\"');
|
||||
str = str.replace(/\0/g, '\\0');
|
||||
return str;
|
||||
};
|
||||
|
||||
// histogram & trends
|
||||
kbn.interval_to_seconds = function(string) {
|
||||
var matches = string.match(/(\d+(?:\.\d+)?)([Mwdhmsy])/);
|
||||
switch (matches[2]) {
|
||||
case 'y':
|
||||
return matches[1]*31536000;
|
||||
case 'M':
|
||||
return matches[1]*2592000;
|
||||
case 'w':
|
||||
return matches[1]*604800;
|
||||
case 'd':
|
||||
return matches[1]*86400;
|
||||
case 'h':
|
||||
return matches[1]*3600;
|
||||
case 'm':
|
||||
return matches[1]*60;
|
||||
case 's':
|
||||
return matches[1];
|
||||
}
|
||||
};
|
||||
|
||||
// This should go away, moment.js can do this
|
||||
kbn.time_ago = function(string) {
|
||||
return new Date(new Date().getTime() - (kbn.interval_to_seconds(string)*1000));
|
||||
};
|
||||
|
||||
// LOL. hahahahaha. DIE.
|
||||
kbn.flatten_json = function(object,root,array) {
|
||||
if (typeof array === 'undefined') {
|
||||
array = {};
|
||||
}
|
||||
if (typeof root === 'undefined') {
|
||||
root = '';
|
||||
}
|
||||
for(var index in object) {
|
||||
var obj = object[index];
|
||||
var rootname = root.length === 0 ? index : root + '.' + index;
|
||||
if(typeof obj === 'object' ) {
|
||||
if(_.isArray(obj)) {
|
||||
if(obj.length > 0 && typeof obj[0] === 'object') {
|
||||
var strval = '';
|
||||
for (var objidx = 0, objlen = obj.length; objidx < objlen; objidx++) {
|
||||
if (objidx > 0) {
|
||||
strval = strval + ', ';
|
||||
}
|
||||
|
||||
strval = strval + JSON.stringify(obj[objidx]);
|
||||
}
|
||||
array[rootname] = strval;
|
||||
} else if(obj.length === 1 && _.isNumber(obj[0])) {
|
||||
array[rootname] = parseFloat(obj[0]);
|
||||
} else {
|
||||
array[rootname] = typeof obj === 'undefined' ? null : obj;
|
||||
}
|
||||
} else {
|
||||
kbn.flatten_json(obj,rootname,array);
|
||||
}
|
||||
} else {
|
||||
array[rootname] = typeof obj === 'undefined' ? null : obj;
|
||||
}
|
||||
}
|
||||
return kbn.sortObj(array);
|
||||
};
|
||||
|
||||
kbn.xmlEnt = function(value) {
|
||||
if(_.isString(value)) {
|
||||
var stg1 = value.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\r\n/g, '<br/>')
|
||||
.replace(/\r/g, '<br/>')
|
||||
.replace(/\n/g, '<br/>')
|
||||
.replace(/\t/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/<del>/g, '<del>')
|
||||
.replace(/<\/del>/g, '</del>');
|
||||
return stg1;
|
||||
} else {
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
kbn.sortObj = function(arr) {
|
||||
// Setup Arrays
|
||||
var sortedKeys = [];
|
||||
var sortedObj = {};
|
||||
var i;
|
||||
// Separate keys and sort them
|
||||
for (i in arr) {
|
||||
sortedKeys.push(i);
|
||||
}
|
||||
sortedKeys.sort();
|
||||
|
||||
// Reconstruct sorted obj based on keys
|
||||
for (i in sortedKeys) {
|
||||
sortedObj[sortedKeys[i]] = arr[sortedKeys[i]];
|
||||
}
|
||||
return sortedObj;
|
||||
};
|
||||
|
||||
kbn.query_color_dot = function (color, diameter) {
|
||||
return '<div class="icon-circle" style="' + [
|
||||
'display:inline-block',
|
||||
'color:' + color,
|
||||
'font-size:' + diameter + 'px',
|
||||
].join(';') + '"></div>';
|
||||
};
|
||||
|
||||
return kbn;
|
||||
});
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* Bootstrap require with the needed config, then load the app.js module.
|
||||
*/
|
||||
require.config({
|
||||
baseUrl: 'app',
|
||||
paths: {
|
||||
settings: 'components/settings',
|
||||
kbn: 'components/kbn',
|
||||
|
||||
css: '../vendor/require/css',
|
||||
text: '../vendor/require/text',
|
||||
moment: '../vendor/moment',
|
||||
filesaver: '../vendor/filesaver',
|
||||
|
||||
angular: '../vendor/angular/angular',
|
||||
'angular-strap': '../vendor/angular/angular-strap',
|
||||
'angular-sanitize': '../vendor/angular/angular-sanitize',
|
||||
timepicker: '../vendor/angular/timepicker',
|
||||
datepicker: '../vendor/angular/datepicker',
|
||||
|
||||
underscore: 'components/underscore.extended',
|
||||
'underscore-src': '../vendor/underscore',
|
||||
bootstrap: '../vendor/bootstrap/bootstrap',
|
||||
|
||||
jquery: 'components/jquery.extended',
|
||||
'jquery-src': '../vendor/jquery/jquery-1.8.0',
|
||||
'jquery.flot': '../vendor/jquery/jquery.flot',
|
||||
'jquery.flot.pie': '../vendor/jquery/jquery.flot.pie',
|
||||
'jquery.flot.selection': '../vendor/jquery/jquery.flot.selection',
|
||||
'jquery.flot.stack': '../vendor/jquery/jquery.flot.stack',
|
||||
'jquery.flot.time': '../vendor/jquery/jquery.flot.time',
|
||||
|
||||
modernizr: '../vendor/modernizr-2.6.1',
|
||||
elasticjs: '../vendor/elasticjs/elastic-angular-client',
|
||||
},
|
||||
shim: {
|
||||
underscore: {
|
||||
// requiring should work, but isn't required
|
||||
exports: '_'
|
||||
},
|
||||
|
||||
angular: {
|
||||
// requiring should work, but isn't required
|
||||
deps: ['jquery'],
|
||||
exports: 'angular'
|
||||
},
|
||||
|
||||
bootstrap: {
|
||||
deps: ['jquery']
|
||||
},
|
||||
|
||||
modernizr: {
|
||||
exports: 'Modernizr'
|
||||
},
|
||||
|
||||
'jquery-src': {
|
||||
// requiring should work, but isn't required
|
||||
exports: 'jQuery'
|
||||
},
|
||||
|
||||
// simple dependency declatation
|
||||
'jquery.flot': ['jquery'],
|
||||
'jquery.flot.pie': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.selection':['jquery', 'jquery.flot'],
|
||||
'jquery.flot.stack': ['jquery', 'jquery.flot'],
|
||||
'jquery.flot.time': ['jquery', 'jquery.flot'],
|
||||
|
||||
'angular-sanitize': ['angular'],
|
||||
'angular-cookies': ['angular'],
|
||||
'angular-loader': ['angular'],
|
||||
'angular-mocks': ['angular'],
|
||||
'angular-resource': ['angular'],
|
||||
'angular-route': ['angular'],
|
||||
'angular-touch': ['angular'],
|
||||
|
||||
'angular-strap': ['angular', 'bootstrap','timepicker', 'datepicker'],
|
||||
|
||||
timepicker: ['jquery', 'bootstrap'],
|
||||
datepicker: ['jquery', 'bootstrap'],
|
||||
|
||||
elasticjs: ['angular', '../vendor/elasticjs/elastic']
|
||||
}
|
||||
});
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
define(['underscore'],
|
||||
function (_) {
|
||||
"use strict";
|
||||
|
||||
return function Settings (options) {
|
||||
/**
|
||||
* To add a setting, you MUST define a default. Also,
|
||||
* THESE ARE ONLY DEFAULTS.
|
||||
* They are overridden by config.js in the root directory
|
||||
* @type {Object}
|
||||
*/
|
||||
var defaults = {
|
||||
elasticsearch : "http://"+window.location.hostname+":9200",
|
||||
panel_names : [],
|
||||
kibana_index : 'kibana-int'
|
||||
};
|
||||
|
||||
// This initializes a new hash on purpose, to avoid adding parameters to
|
||||
// config.js without providing sane defaults
|
||||
var settings = {};
|
||||
_.each(defaults, function(value, key) {
|
||||
settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
|
||||
});
|
||||
|
||||
return settings;
|
||||
};
|
||||
});
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
define([
|
||||
'underscore-src'
|
||||
],
|
||||
function () {
|
||||
'use strict';
|
||||
|
||||
var _ = window._;
|
||||
|
||||
/*
|
||||
Mixins :)
|
||||
*/
|
||||
_.mixin({
|
||||
move: function (array, fromIndex, toIndex) {
|
||||
array.splice(toIndex, 0, array.splice(fromIndex, 1)[0] );
|
||||
return array;
|
||||
},
|
||||
remove: function (array, index) {
|
||||
array.splice(index, 1);
|
||||
return array;
|
||||
},
|
||||
toggleInOut: function(array,value) {
|
||||
if(_.contains(array,value)) {
|
||||
array = _.without(array,value);
|
||||
} else {
|
||||
array.push(value);
|
||||
}
|
||||
return array;
|
||||
}
|
||||
});
|
||||
|
||||
return _;
|
||||
});
|
||||
Executable
+54
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* These is the app's configuration, If you need to configure
|
||||
* the default dashboard, please see dashboards/default
|
||||
*/
|
||||
define(['settings'],
|
||||
function (Settings) {
|
||||
"use strict";
|
||||
|
||||
return new Settings({
|
||||
|
||||
/**
|
||||
* URL to your elasticsearch server. You almost certainly don't
|
||||
* want 'http://localhost:9200' here. Even if Kibana and ES are on
|
||||
* the same host
|
||||
*
|
||||
* By default this will attempt to reach ES at the same host you have
|
||||
* elasticsearch installed on. You probably want to set it to the FQDN of your
|
||||
* elasticsearch host
|
||||
* @type {String}
|
||||
*/
|
||||
elasticsearch: "http://"+window.location.hostname+":9200",
|
||||
|
||||
/**
|
||||
* The default ES index to use for storing Kibana specific object
|
||||
* such as stored dashboards
|
||||
* @type {String}
|
||||
*/
|
||||
kibana_index: "kibana-int",
|
||||
|
||||
/**
|
||||
* Panel modules available. Panels will only be loaded when they are defined in the
|
||||
* dashboard, but this list is used in the "add panel" interface.
|
||||
* @type {Array}
|
||||
*/
|
||||
panel_names: [
|
||||
'histogram',
|
||||
'map',
|
||||
'pie',
|
||||
'table',
|
||||
'filtering',
|
||||
'timepicker',
|
||||
'text',
|
||||
'fields',
|
||||
'hits',
|
||||
'dashcontrol',
|
||||
'column',
|
||||
'derivequeries',
|
||||
'trends',
|
||||
'bettermap',
|
||||
'query',
|
||||
'terms'
|
||||
]
|
||||
});
|
||||
});
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
define([
|
||||
'./dash',
|
||||
'./dashLoader',
|
||||
'./row',
|
||||
], function () {});
|
||||
Executable
+73
@@ -0,0 +1,73 @@
|
||||
define([
|
||||
'angular',
|
||||
'config',
|
||||
'underscore',
|
||||
|
||||
'services/all'
|
||||
],
|
||||
function (angular, config, _) {
|
||||
"use strict";
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
|
||||
module.controller('DashCtrl', function($scope, $route, ejsResource, fields, dashboard, alertSrv) {
|
||||
$scope.editor = {
|
||||
index: 0
|
||||
};
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.config = config;
|
||||
// Make underscore.js available to views
|
||||
$scope._ = _;
|
||||
$scope.dashboard = dashboard;
|
||||
$scope.dashAlerts = alertSrv;
|
||||
alertSrv.clearAll();
|
||||
|
||||
// Provide a global list of all see fields
|
||||
$scope.fields = fields;
|
||||
$scope.reset_row();
|
||||
|
||||
$scope.ejs = ejsResource(config.elasticsearch);
|
||||
};
|
||||
|
||||
$scope.add_row = function(dash,row) {
|
||||
dash.rows.push(row);
|
||||
};
|
||||
|
||||
$scope.reset_row = function() {
|
||||
$scope.row = {
|
||||
title: '',
|
||||
height: '150px',
|
||||
editable: true,
|
||||
};
|
||||
};
|
||||
|
||||
$scope.row_style = function(row) {
|
||||
return { 'min-height': row.collapse ? '5px' : row.height };
|
||||
};
|
||||
|
||||
$scope.edit_path = function(type) {
|
||||
if(type) {
|
||||
return 'app/panels/'+type+'/editor.html';
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.setEditorTabs = function(panelMeta) {
|
||||
$scope.editorTabs = ['General','Panel'];
|
||||
if(!_.isUndefined(panelMeta.editorTabs)) {
|
||||
$scope.editorTabs = _.union($scope.editorTabs,_.pluck(panelMeta.editorTabs,'title'));
|
||||
}
|
||||
return $scope.editorTabs;
|
||||
};
|
||||
|
||||
// This is whoafully incomplete, but will do for now
|
||||
$scope.parse_error = function(data) {
|
||||
var _error = data.match("nested: (.*?);");
|
||||
return _.isNull(_error) ? data : _error[1];
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
});
|
||||
});
|
||||
Executable
+123
@@ -0,0 +1,123 @@
|
||||
define([
|
||||
'angular',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
|
||||
module.controller('dashLoader', function($scope, $http, timer, dashboard, alertSrv) {
|
||||
$scope.loader = dashboard.current.loader;
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
$scope.gist = $scope.gist || {};
|
||||
$scope.elasticsearch = $scope.elasticsearch || {};
|
||||
};
|
||||
|
||||
$scope.showDropdown = function(type) {
|
||||
var _l = $scope.loader;
|
||||
if(type === 'load') {
|
||||
return (_l.load_elasticsearch || _l.load_gist || _l.load_local);
|
||||
}
|
||||
if(type === 'save') {
|
||||
return (_l.save_elasticsearch || _l.save_gist || _l.local_local || _l.save_default);
|
||||
}
|
||||
if(type === 'share') {
|
||||
return (_l.save_temp);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.set_default = function() {
|
||||
if(dashboard.set_default()) {
|
||||
alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.purge_default = function() {
|
||||
if(dashboard.purge_default()) {
|
||||
alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.elasticsearch_save = function(type,ttl) {
|
||||
dashboard.elasticsearch_save(
|
||||
type,
|
||||
($scope.elasticsearch.title || dashboard.current.title),
|
||||
($scope.loader.save_temp_ttl_enable ? ttl : false)
|
||||
).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result._id)) {
|
||||
alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' +
|
||||
result._id + '"','success',5000);
|
||||
if(type === 'temp') {
|
||||
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
|
||||
}
|
||||
} else {
|
||||
alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.elasticsearch_delete = function(id) {
|
||||
dashboard.elasticsearch_delete(id).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result)) {
|
||||
if(result.found) {
|
||||
alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000);
|
||||
// Find the deleted dashboard in the cached list and remove it
|
||||
var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0];
|
||||
$scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete);
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000);
|
||||
}
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.elasticsearch_dblist = function(query) {
|
||||
dashboard.elasticsearch_list(query,$scope.loader.load_elasticsearch_size).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result.hits)) {
|
||||
$scope.hits = result.hits.total;
|
||||
$scope.elasticsearch.dashboards = result.hits.hits;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.save_gist = function() {
|
||||
dashboard.save_gist($scope.gist.title).then(
|
||||
function(link) {
|
||||
if(!_.isUndefined(link)) {
|
||||
$scope.gist.last = link;
|
||||
alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+
|
||||
'<a href="'+link+'">'+link+'</a> in a moment','success');
|
||||
} else {
|
||||
alertSrv.set('Save failed','Gist could not be saved','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.gist_dblist = function(id) {
|
||||
dashboard.gist_list(id).then(
|
||||
function(files) {
|
||||
if(files && files.length > 0) {
|
||||
$scope.gist.files = files;
|
||||
} else {
|
||||
alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.controllers');
|
||||
|
||||
module.controller('RowCtrl', function($scope, $rootScope, $timeout,ejsResource, querySrv) {
|
||||
var _d = {
|
||||
title: "Row",
|
||||
height: "150px",
|
||||
collapse: false,
|
||||
collapsable: true,
|
||||
editable: true,
|
||||
panels: [],
|
||||
};
|
||||
|
||||
_.defaults($scope.row,_d);
|
||||
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.querySrv = querySrv;
|
||||
$scope.reset_panel();
|
||||
};
|
||||
|
||||
$scope.toggle_row = function(row) {
|
||||
if(!row.collapsable) {
|
||||
return;
|
||||
}
|
||||
row.collapse = row.collapse ? false : true;
|
||||
if (!row.collapse) {
|
||||
$timeout(function() {
|
||||
$scope.$broadcast('render');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// This can be overridden by individual panels
|
||||
$scope.close_edit = function() {
|
||||
$scope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.add_panel = function(row,panel) {
|
||||
$scope.row.panels.push(panel);
|
||||
};
|
||||
|
||||
$scope.reset_panel = function(type) {
|
||||
$scope.panel = {
|
||||
error : false,
|
||||
span : 3,
|
||||
editable: true,
|
||||
type : type
|
||||
};
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
Executable
+41
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"title": "",
|
||||
"services": {
|
||||
"query": {
|
||||
"idQueue": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"query": "*",
|
||||
"alias": "",
|
||||
"color": "#7EB26D",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"idQueue": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"list": {},
|
||||
"ids": []
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
],
|
||||
"editable": true,
|
||||
"index": {
|
||||
"interval": "none",
|
||||
"pattern": "[logstash-]YYYY.MM.DD",
|
||||
"default": "NOT_CONFIGURED"
|
||||
}
|
||||
}
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
{
|
||||
"title": "Introduction",
|
||||
"services": {
|
||||
"query": {
|
||||
"idQueue": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"query": "*",
|
||||
"alias": "",
|
||||
"color": "#7EB26D",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"idQueue": [
|
||||
1,
|
||||
2
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"from": "2013-07-27T22:08:06.800Z",
|
||||
"to": "2013-07-27T23:08:06.801Z",
|
||||
"field": "@timestamp",
|
||||
"type": "time",
|
||||
"mandate": "must",
|
||||
"active": true,
|
||||
"alias": "",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"title": "Intro",
|
||||
"height": "450px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": false,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 4,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"mode": "markdown",
|
||||
"content": " \n\n##### Did you just upgrade? Not expecting this screen?\nIf you were using the old default page you might not be expecting this screen. I understand, change can be awkward. Let me explain. \n\n##### Setting a global default dashboard\nKibana has always shipped with an interface for Logstash, still does! You can access it [here](index.html#dashboard/file/logstash.json). However, if you want to make it your default again, all you you need to do is rename a file!\nIn your Kibana installation directory: \n\nRename *logstash.json* to *default.json* and refresh. Should be all set.\n\n##### But wait, there's more!\nIn fact, you can add any exported dashboard to that directory and access it as *http://YOUR-HOST -HERE/index.html#dashboard/file/YOUR-DASHBOARD.json*. Neat trick eh?",
|
||||
"style": {},
|
||||
"title": "",
|
||||
"status": "Stable"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 1,
|
||||
"editable": false,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"mode": "markdown",
|
||||
"content": "",
|
||||
"style": {},
|
||||
"status": "Stable"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 7,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"mode": "markdown",
|
||||
"content": "### Welcome to Kibana. \nGlad you could make it. Happy to have you here! Lets get started, shall we?\n##### Requirements\n* **A good browser.** \n The latest version of Chrome or Firefox is recommended. Safari (latest version) and Internet Explorer 9 and above are also supported.\n* **A webserver.** \n Just somewhere to host the HTML and Javascript. Basically any webserver will work.\n* **Elasticsearch** \n 0.20.5 or above. Kibana will soon move to requiring Elasticsearch 0.90 or above, so upgrading is recommended.\n\n##### Configuration\nIf Kibana and Elasticsearch are on the same host, and you're using the default Elasticsearch port, then you're all set. Kibana is configured to use that setup by default! \n\nIf not, you need to edit *config.js* and set the *elasticsearch* parameter with the URL (including port, probably 9200) of your Elasticsearch server. The host part should be the entire, fully qualified domain name, or IP, **not localhost**.\n#### Are you a Logstash User?\n+ **YES** - Great! We have a prebuilt dashboard: [(Logstash Dashboard)](index.html#/dashboard/file/logstash.json). See the note to the right about making it your global default \n\n+ **NO** - Hey, no problem, you just have a bit of setup to do. You have a few choices: \n\n 1. [Sample Dashboard](index.html#/dashboard/file/guided.json) *I don't have much data yet, please extract some basics for me* \n 2. [Unconfigured Dashboard](index.html#/dashboard/file/noted.json) *I have a lot of data and I don't want Kibana to query it at once*\n 3. [Blank Dashboard](index.html#/dashboard/file/blank.json) *I'm comfortable figuring it out on my own*",
|
||||
"style": {},
|
||||
"status": "Stable"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"index": {
|
||||
"interval": "none",
|
||||
"pattern": "[logstash-]YYYY.MM.DD",
|
||||
"default": "_all"
|
||||
}
|
||||
}
|
||||
Executable
+246
@@ -0,0 +1,246 @@
|
||||
{
|
||||
"title": "Your Basic Dashboard",
|
||||
"services": {
|
||||
"query": {
|
||||
"idQueue": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"query": "*",
|
||||
"alias": "",
|
||||
"color": "#7EB26D",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"idQueue": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"list": {},
|
||||
"ids": []
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"title": "Options",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 5,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "If you have a field with a timestamp in it, you might want to add a 'timepicker' panel here. Click the cog icon over to the left to do so. You can also remove these information text panels there",
|
||||
"style": {},
|
||||
"title": "Have a timestamp somewhere?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 5,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "query",
|
||||
"label": "Search",
|
||||
"history": [
|
||||
"*"
|
||||
],
|
||||
"remember": 10,
|
||||
"pinned": true,
|
||||
"query": "*"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 7,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "#### Filtering\nSee the small *Filters* text to the left below? Click it to expand the filters row. Right now there are none. click on one of the icons in the document types list to filter down to only that document type",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Filters",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": true,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "filtering"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Graph",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 3,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "terms",
|
||||
"queries": {
|
||||
"mode": "all",
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"field": "_type",
|
||||
"exclude": [],
|
||||
"missing": true,
|
||||
"other": true,
|
||||
"size": 100,
|
||||
"order": "count",
|
||||
"style": {
|
||||
"font-size": "10pt"
|
||||
},
|
||||
"donut": false,
|
||||
"tilt": false,
|
||||
"labels": true,
|
||||
"arrangement": "horizontal",
|
||||
"chart": "pie",
|
||||
"counter_pos": "none",
|
||||
"title": "Document types"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 3,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "terms",
|
||||
"queries": {
|
||||
"mode": "all",
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"field": "_type",
|
||||
"exclude": [],
|
||||
"missing": true,
|
||||
"other": true,
|
||||
"size": 10,
|
||||
"order": "count",
|
||||
"style": {
|
||||
"font-size": "10pt"
|
||||
},
|
||||
"donut": false,
|
||||
"tilt": false,
|
||||
"labels": true,
|
||||
"arrangement": "horizontal",
|
||||
"chart": "table",
|
||||
"counter_pos": "above"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 6,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "### The most generic dashboard ever\nIt's the best I can do without knowing much about your data! I've tried to pick some sane defaults for you. The two *terms* panels to the left of this *text* panel show a breakdown of your document type. \n\nKibana is currently configured to point at the special Elasticsearch *_all* index. You can change that by clicking on the cog icon in the title bar. You can also add rows from that dialog. You can edit individual panels by click on the link that appears in their top right when yuo mouse over them\n\nThe *table* panel below has attempted to list your fields to the left, select a few to view them in the table. To add more panels, of different types, click the cog on the row label to the far left",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Events",
|
||||
"height": "650px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "table",
|
||||
"size": 100,
|
||||
"pages": 5,
|
||||
"offset": 0,
|
||||
"sort": [
|
||||
"_id",
|
||||
"desc"
|
||||
],
|
||||
"style": {
|
||||
"font-size": "9pt"
|
||||
},
|
||||
"overflow": "min-height",
|
||||
"fields": [],
|
||||
"highlight": [],
|
||||
"sortable": true,
|
||||
"header": true,
|
||||
"paging": true,
|
||||
"spyable": true,
|
||||
"queries": {
|
||||
"mode": "all",
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"field_list": true,
|
||||
"status": "Stable"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"index": {
|
||||
"interval": "none",
|
||||
"pattern": "[logstash-]YYYY.MM.DD",
|
||||
"default": "_all"
|
||||
}
|
||||
}
|
||||
Executable
+172
@@ -0,0 +1,172 @@
|
||||
/* global _, kbn */
|
||||
|
||||
/*
|
||||
* Complex scripted Logstash dashboard
|
||||
* This script generates a dashboard object that Kibana can load. It also takes a number of user
|
||||
* supplied URL parameters, none are required:
|
||||
*
|
||||
* index :: Which index to search? If this is specified, interval is set to 'none'
|
||||
* pattern :: Does nothing if index is specified. Set a timestamped index pattern. Default: [logstash-]YYYY.MM.DD
|
||||
* interval :: Sets the index interval (eg: day,week,month,year), Default: day
|
||||
*
|
||||
* split :: The character to split the queries on Default: ','
|
||||
* query :: By default, a comma seperated list of queries to run. Default: *
|
||||
*
|
||||
* from :: Search this amount of time back, eg 15m, 1h, 2d. Default: 15m
|
||||
* timefield :: The field containing the time to filter on, Default: @timestamp
|
||||
*
|
||||
* fields :: comma seperated list of fields to show in the table
|
||||
* sort :: comma seperated field to sort on, and direction, eg sort=@timestamp,desc
|
||||
*
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
// Setup some variables
|
||||
var dashboard, queries, _d_timespan;
|
||||
|
||||
// All url parameters are available via the ARGS object
|
||||
var ARGS;
|
||||
|
||||
// Set a default timespan if one isn't specified
|
||||
_d_timespan = '1h';
|
||||
|
||||
// Intialize a skeleton with nothing but a rows array and service object
|
||||
dashboard = {
|
||||
rows : [],
|
||||
services : {}
|
||||
};
|
||||
|
||||
// Set a title
|
||||
dashboard.title = 'Logstash Search';
|
||||
|
||||
// Allow the user to set the index, if they dont, fall back to logstash.
|
||||
if(!_.isUndefined(ARGS.index)) {
|
||||
dashboard.index = {
|
||||
default: ARGS.index,
|
||||
interval: 'none'
|
||||
};
|
||||
} else {
|
||||
// Don't fail to default
|
||||
dashboard.failover = false;
|
||||
dashboard.index = {
|
||||
default: ARGS.index||'ADD_A_TIME_FILTER',
|
||||
pattern: ARGS.pattern||'[logstash-]YYYY.MM.DD',
|
||||
interval: ARGS.interval||'day'
|
||||
};
|
||||
}
|
||||
|
||||
// In this dashboard we let users pass queries as comma seperated list to the query parameter.
|
||||
// Or they can specify a split character using the split aparameter
|
||||
// If query is defined, split it into a list of query objects
|
||||
// NOTE: ids must be integers, hence the parseInt()s
|
||||
if(!_.isUndefined(ARGS.query)) {
|
||||
queries = _.object(_.map(ARGS.query.split(ARGS.split||','), function(v,k) {
|
||||
return [k,{
|
||||
query: v,
|
||||
id: parseInt(k,10),
|
||||
alias: v
|
||||
}];
|
||||
}));
|
||||
} else {
|
||||
// No queries passed? Initialize a single query to match everything
|
||||
queries = {
|
||||
0: {
|
||||
query: '*',
|
||||
id: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Now populate the query service with our objects
|
||||
dashboard.services.query = {
|
||||
list : queries,
|
||||
ids : _.map(_.keys(queries),function(v){return parseInt(v,10);})
|
||||
};
|
||||
|
||||
// Lets also add a default time filter, the value of which can be specified by the user
|
||||
// This isn't strictly needed, but it gets rid of the info alert about the missing time filter
|
||||
dashboard.services.filter = {
|
||||
list: {
|
||||
0: {
|
||||
from: kbn.time_ago(ARGS.from||_d_timespan),
|
||||
to: new Date(),
|
||||
field: ARGS.timefield||"@timestamp",
|
||||
type: "time",
|
||||
active: true,
|
||||
id: 0
|
||||
}
|
||||
},
|
||||
ids: [0]
|
||||
};
|
||||
|
||||
// Ok, lets make some rows. The Filters row is collapsed by default
|
||||
dashboard.rows = [
|
||||
{
|
||||
title: "Options",
|
||||
height: "30px"
|
||||
},
|
||||
{
|
||||
title: "Query",
|
||||
height: "30px"
|
||||
},
|
||||
{
|
||||
title: "Filters",
|
||||
height: "100px",
|
||||
collapse: true
|
||||
},
|
||||
{
|
||||
title: "Chart",
|
||||
height: "300px"
|
||||
},
|
||||
{
|
||||
title: "Events",
|
||||
height: "400px"
|
||||
}
|
||||
];
|
||||
|
||||
// Setup some panels. A query panel and a filter panel on the same row
|
||||
dashboard.rows[0].panels = [
|
||||
{
|
||||
type: 'timepicker',
|
||||
span: 6,
|
||||
timespan: ARGS.from||_d_timespan
|
||||
}
|
||||
];
|
||||
|
||||
// Add a filtering panel to the 3rd row
|
||||
dashboard.rows[1].panels = [
|
||||
{
|
||||
type: 'Query'
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
// Add a filtering panel to the 3rd row
|
||||
dashboard.rows[2].panels = [
|
||||
{
|
||||
type: 'filtering'
|
||||
}
|
||||
];
|
||||
|
||||
// And a histogram that allows the user to specify the interval and time field
|
||||
dashboard.rows[3].panels = [
|
||||
{
|
||||
type: 'histogram',
|
||||
time_field: ARGS.timefield||"@timestamp",
|
||||
auto_int: true
|
||||
}
|
||||
];
|
||||
|
||||
// And a table row where you can specify field and sort order
|
||||
dashboard.rows[4].panels = [
|
||||
{
|
||||
type: 'table',
|
||||
fields: !_.isUndefined(ARGS.fields) ? ARGS.fields.split(',') : [],
|
||||
sort: !_.isUndefined(ARGS.sort) ? ARGS.sort.split(',') : [ARGS.timefield||'@timestamp','desc'],
|
||||
overflow: 'expand'
|
||||
}
|
||||
];
|
||||
|
||||
// Now return the object and we're good!
|
||||
return dashboard;
|
||||
Executable
+217
@@ -0,0 +1,217 @@
|
||||
{
|
||||
"title": "Logstash Search",
|
||||
"services": {
|
||||
"query": {
|
||||
"idQueue": [
|
||||
1
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"query": "{{ARGS.query || '*'}}",
|
||||
"alias": "",
|
||||
"color": "#7EB26D",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"idQueue": [
|
||||
1
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"from": "2013-07-30T18:58:13.977Z",
|
||||
"to": "2013-07-30T19:58:13.977Z",
|
||||
"field": "@timestamp",
|
||||
"type": "time",
|
||||
"mandate": "must",
|
||||
"active": true,
|
||||
"alias": "",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"title": "Options",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": "",
|
||||
"span": 6,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "timepicker",
|
||||
"mode": "relative",
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
],
|
||||
"timespan": "{{ARGS.from || '1h'}}",
|
||||
"timefield": "@timestamp",
|
||||
"timeformat": "",
|
||||
"refresh": {
|
||||
"enable": false,
|
||||
"interval": 30,
|
||||
"min": 3
|
||||
},
|
||||
"filter_id": 0,
|
||||
"status": "Stable"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "query",
|
||||
"label": "Search",
|
||||
"history": [],
|
||||
"remember": 10,
|
||||
"pinned": true,
|
||||
"query": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Filters",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": true,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "filtering"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Graph",
|
||||
"height": "350px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "histogram",
|
||||
"mode": "count",
|
||||
"time_field": "@timestamp",
|
||||
"value_field": null,
|
||||
"auto_int": true,
|
||||
"resolution": 100,
|
||||
"interval": "30s",
|
||||
"fill": 3,
|
||||
"linewidth": 3,
|
||||
"timezone": "browser",
|
||||
"spyable": true,
|
||||
"zoomlinks": true,
|
||||
"bars": true,
|
||||
"stack": true,
|
||||
"points": false,
|
||||
"lines": false,
|
||||
"legend": true,
|
||||
"x-axis": true,
|
||||
"y-axis": true,
|
||||
"percentage": false,
|
||||
"interactive": true,
|
||||
"queries": {
|
||||
"mode": "all",
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"title": "Events over time"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Events",
|
||||
"height": "350px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "table",
|
||||
"size": 100,
|
||||
"pages": 5,
|
||||
"offset": 0,
|
||||
"sort": [
|
||||
"@timestamp",
|
||||
"desc"
|
||||
],
|
||||
"style": {
|
||||
"font-size": "9pt"
|
||||
},
|
||||
"overflow": "min-height",
|
||||
"fields": [],
|
||||
"highlight": [],
|
||||
"sortable": true,
|
||||
"header": true,
|
||||
"paging": true,
|
||||
"spyable": true,
|
||||
"queries": {
|
||||
"mode": "all",
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"field_list": true,
|
||||
"status": "Stable"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"index": {
|
||||
"interval": "day",
|
||||
"pattern": "[logstash-]YYYY.MM.DD",
|
||||
"default": "NO_TIME_FILTER_OR_INDEX_PATTERN_NOT_MATCHED"
|
||||
}
|
||||
}
|
||||
Executable
+178
@@ -0,0 +1,178 @@
|
||||
{
|
||||
"title": "Your Basic Dashboard",
|
||||
"services": {
|
||||
"query": {
|
||||
"idQueue": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4
|
||||
],
|
||||
"list": {
|
||||
"0": {
|
||||
"query": "*",
|
||||
"alias": "",
|
||||
"color": "#7EB26D",
|
||||
"id": 0
|
||||
}
|
||||
},
|
||||
"ids": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"filter": {
|
||||
"idQueue": [
|
||||
0,
|
||||
1,
|
||||
2
|
||||
],
|
||||
"list": {},
|
||||
"ids": []
|
||||
}
|
||||
},
|
||||
"rows": [
|
||||
{
|
||||
"title": "Options",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 5,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "If you have a field with a timestamp in it, you might want to add a 'timepicker' panel here. Click the cog icon over to the left to do so. You can also remove these information text panels there",
|
||||
"style": {},
|
||||
"title": "Have a timestamp somewhere?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Query",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "query",
|
||||
"label": "Search",
|
||||
"history": [
|
||||
"*"
|
||||
],
|
||||
"remember": 10,
|
||||
"pinned": true,
|
||||
"query": "*"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Filters",
|
||||
"height": "50px",
|
||||
"editable": true,
|
||||
"collapse": true,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 3,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "You found the filter row! This row has a 'filtering' panel in it that lists any active filters. You usually want one of these on any dashboard.",
|
||||
"style": {}
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 9,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "filtering"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Graph",
|
||||
"height": "250px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 4,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "See the small Filters text above this? Click it to expand the filters row. Right now there are none, but if you were to add a Table panel, you could click on event fields to drill down and add some. Or if you had timestamped data and used a time picker, your time filter would appear there",
|
||||
"style": {},
|
||||
"title": "Filtering"
|
||||
},
|
||||
{
|
||||
"error": false,
|
||||
"span": 8,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "### Start here\nThis dashboard doesn't run any queries, but it's the best I can do without knowing much about your data!\n\n##### Kibana is currently configured to point at the special Elasticsearch *_all* index. You can change that by clicking on the cog icon in the title bar of this dashboard\nIf you have several indices and a lot of data, you should probably do that before you add any new panels. You can also add rows from that dialog. You can edit individual panels by click on the link that appears in their top right when you mouse over them",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Table",
|
||||
"height": "650px",
|
||||
"editable": true,
|
||||
"collapse": false,
|
||||
"collapsable": true,
|
||||
"panels": [
|
||||
{
|
||||
"error": false,
|
||||
"span": 12,
|
||||
"editable": true,
|
||||
"group": [
|
||||
"default"
|
||||
],
|
||||
"type": "text",
|
||||
"status": "Stable",
|
||||
"mode": "markdown",
|
||||
"content": "## A good place for a table\nThis is a good place for a table panel. Table panels present your data in a tabular format and allow you pick the fields you want to see, sort on them, and drill down.",
|
||||
"style": {}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"editable": true,
|
||||
"index": {
|
||||
"interval": "none",
|
||||
"pattern": "[logstash-]YYYY.MM.DD",
|
||||
"default": "_all"
|
||||
}
|
||||
}
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('addPanel', function($compile) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function($scope, elem) {
|
||||
$scope.$watch('panel.type', function() {
|
||||
var _type = $scope.panel.type;
|
||||
$scope.reset_panel(_type);
|
||||
if(!_.isUndefined($scope.panel.type)) {
|
||||
$scope.panel.loadingEditor = true;
|
||||
$scope.require(['panels/'+$scope.panel.type+'/module'], function () {
|
||||
var template = '<div ng-controller="'+$scope.panel.type+'" ng-include="\'app/partials/paneladd.html\'"></div>';
|
||||
elem.html($compile(angular.element(template))($scope));
|
||||
$scope.panel.loadingEditor = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
define([
|
||||
'./addPanel',
|
||||
'./arrayJoin',
|
||||
'./dashUpload',
|
||||
'./kibanaPanel',
|
||||
'./ngBlur',
|
||||
'./ngModelOnBlur',
|
||||
'./tip'
|
||||
], function () {});
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('arrayJoin', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, element, attr, ngModel) {
|
||||
|
||||
function split_array(text) {
|
||||
return (text || '').split(',');
|
||||
}
|
||||
|
||||
function join_array(text) {
|
||||
if(_.isArray(text)) {
|
||||
return (text || '').join(',');
|
||||
} else {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
ngModel.$parsers.push(split_array);
|
||||
ngModel.$formatters.push(join_array);
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
define([
|
||||
'angular'
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.directives');
|
||||
|
||||
module.directive('dashUpload', function(timer, dashboard, alertSrv){
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope) {
|
||||
function file_selected(evt) {
|
||||
var files = evt.target.files; // FileList object
|
||||
var readerOnload = function() {
|
||||
return function(e) {
|
||||
dashboard.dash_load(JSON.parse(e.target.result));
|
||||
scope.$apply();
|
||||
};
|
||||
};
|
||||
for (var i = 0, f; f = files[i]; i++) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (readerOnload)(f);
|
||||
reader.readAsText(f);
|
||||
}
|
||||
}
|
||||
// Check for the various File API support.
|
||||
if (window.File && window.FileReader && window.FileList && window.Blob) {
|
||||
// Something
|
||||
document.getElementById('dashupload').addEventListener('change', file_selected, false);
|
||||
} else {
|
||||
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
define([
|
||||
'angular'
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('kibanaPanel', function($compile) {
|
||||
var editorTemplate =
|
||||
'<i class="icon-spinner small icon-spin icon-large panel-loading"' +
|
||||
'ng-show="panelMeta.loading == true && !panel.title"></i>' +
|
||||
'<span class="editlink panelextra pointer" style="right:15px;top:0px"' +
|
||||
'bs-modal="\'app/partials/paneleditor.html\'" ng-show="panel.editable != false">' +
|
||||
'<span class="small">{{panel.type}}</span> <i class="icon-cog pointer"></i></span>' +
|
||||
'<h4 ng-show="panel.title">' +
|
||||
'{{panel.title}}' +
|
||||
'<i class="icon-spinner smaller icon-spin icon-large"' +
|
||||
'ng-show="panelMeta.loading == true && panel.title"></i>' +
|
||||
'</h4>';
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function($scope, elem, attr) {
|
||||
// once we have the template, scan it for controllers and
|
||||
// load the module.js if we have any
|
||||
|
||||
// compile the module and uncloack. We're done
|
||||
function loadModule($module) {
|
||||
$module.appendTo(elem);
|
||||
/* jshint indent:false */
|
||||
$compile(elem.contents())($scope);
|
||||
elem.removeClass("ng-cloak");
|
||||
}
|
||||
|
||||
$scope.$watch(attr.type, function (name) {
|
||||
elem.addClass("ng-cloak");
|
||||
// load the panels module file, then render it in the dom.
|
||||
$scope.require([
|
||||
'jquery',
|
||||
'text!panels/'+name+'/module.html'
|
||||
], function ($, moduleTemplate) {
|
||||
var $module = $(moduleTemplate);
|
||||
// top level controllers
|
||||
var $controllers = $module.filter('ngcontroller, [ng-controller], .ng-controller');
|
||||
// add child controllers
|
||||
$controllers = $controllers.add($module.find('ngcontroller, [ng-controller], .ng-controller'));
|
||||
|
||||
if ($controllers.length) {
|
||||
$controllers.first().prepend(editorTemplate);
|
||||
$scope.require([
|
||||
'panels/'+name+'/module'
|
||||
], function() {
|
||||
loadModule($module);
|
||||
});
|
||||
} else {
|
||||
loadModule($module);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
define([
|
||||
'angular'
|
||||
],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('ngBlur', ['$parse', function($parse) {
|
||||
return function(scope, element, attr) {
|
||||
var fn = $parse(attr['ngBlur']);
|
||||
element.bind('blur', function(event) {
|
||||
scope.$apply(function() {
|
||||
fn(scope, {$event:event});
|
||||
});
|
||||
});
|
||||
};
|
||||
}]);
|
||||
|
||||
});
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
define(['angular'],
|
||||
function (angular) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('ngModelOnblur', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attr, ngModelCtrl) {
|
||||
if (attr.type === 'radio' || attr.type === 'checkbox') {
|
||||
return;
|
||||
}
|
||||
|
||||
elm.unbind('input').unbind('keydown').unbind('change');
|
||||
elm.bind('blur', function() {
|
||||
scope.$apply(function() {
|
||||
ngModelCtrl.$setViewValue(elm.val());
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
define([
|
||||
'angular',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, kbn) {
|
||||
'use strict';
|
||||
|
||||
angular
|
||||
.module('kibana.directives')
|
||||
.directive('tip', function($compile) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, elem, attrs) {
|
||||
var _t = '<i class="icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
|
||||
kbn.addslashes(elem.text())+'\'"></i>';
|
||||
elem.replaceWith($compile(angular.element(_t))(scope));
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+110
@@ -0,0 +1,110 @@
|
||||
define(['angular', 'jquery', 'underscore'], function (angular, $, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.filters');
|
||||
|
||||
module.filter('stringSort', function() {
|
||||
return function(input) {
|
||||
return input.sort();
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('pinnedQuery', function(querySrv) {
|
||||
return function( items, pinned) {
|
||||
var ret = _.filter(querySrv.ids,function(id){
|
||||
var v = querySrv.list[id];
|
||||
if(!_.isUndefined(v.pin) && v.pin === true && pinned === true) {
|
||||
return true;
|
||||
}
|
||||
if((_.isUndefined(v.pin) || v.pin === false) && pinned === false) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('slice', function() {
|
||||
return function(arr, start, end) {
|
||||
if(!_.isUndefined(arr)) {
|
||||
return arr.slice(start, end);
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('stringify', function() {
|
||||
return function(arr) {
|
||||
if(_.isObject(arr) && !_.isArray(arr)) {
|
||||
return angular.toJson(arr);
|
||||
} else {
|
||||
return arr.toString();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('noXml', function() {
|
||||
var noXml = function(text) {
|
||||
return _.isString(text)
|
||||
? text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/"/g, '"')
|
||||
: text;
|
||||
};
|
||||
return function(text) {
|
||||
return _.isArray(text)
|
||||
? _.map(text, noXml)
|
||||
: noXml(text);
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('urlLink', function() {
|
||||
var //URLs starting with http://, https://, or ftp://
|
||||
r1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim,
|
||||
//URLs starting with "www." (without // before it, or it'd re-link the ones done above).
|
||||
r2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim,
|
||||
//Change email addresses to mailto:: links.
|
||||
r3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim;
|
||||
|
||||
var urlLink = function(text) {
|
||||
var t1,t2,t3;
|
||||
if(!_.isString(text)) {
|
||||
return text;
|
||||
} else {
|
||||
_.each(text.match(r1), function() {
|
||||
t1 = text.replace(r1, "<a href=\"$1\" target=\"_blank\">$1</a>");
|
||||
});
|
||||
text = t1 || text;
|
||||
_.each(text.match(r2), function() {
|
||||
t2 = text.replace(r2, "$1<a href=\"http://$2\" target=\"_blank\">$2</a>");
|
||||
});
|
||||
text = t2 || text;
|
||||
_.each(text.match(r3), function() {
|
||||
t3 = text.replace(r3, "<a href=\"mailto:$1\">$1</a>");
|
||||
});
|
||||
text = t3 || text;
|
||||
return text;
|
||||
}
|
||||
};
|
||||
return function(text) {
|
||||
return _.isArray(text)
|
||||
? _.map(text, urlLink)
|
||||
: urlLink(text);
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('gistid', function() {
|
||||
var gist_pattern = /(\d{5,})|([a-z0-9]{10,})|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
return function(input) {
|
||||
if(!(_.isUndefined(input))) {
|
||||
var output = input.match(gist_pattern);
|
||||
if(!_.isNull(output) && !_.isUndefined(output)) {
|
||||
return output[0].replace(/.*\//, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+17
@@ -0,0 +1,17 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<form>
|
||||
<h6>Coordinate Field <tip>geoJSON array! Long,Lat NOT Lat,Long</tip></h6>
|
||||
<input bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.field">
|
||||
</form>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<form>
|
||||
<h6>Tooltip Field</h6>
|
||||
<input bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.tooltip">
|
||||
</form>
|
||||
</div>
|
||||
<div class="span2"><h6>Max Points</h6>
|
||||
<input type="number" class="input-small" ng-model="panel.size">
|
||||
</div>
|
||||
</div>
|
||||
Executable
BIN
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 797 B |
+8724
File diff suppressed because it is too large
Load Diff
Executable
+463
@@ -0,0 +1,463 @@
|
||||
/* required styles */
|
||||
|
||||
.leaflet-map-pane,
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow,
|
||||
.leaflet-tile-pane,
|
||||
.leaflet-tile-container,
|
||||
.leaflet-overlay-pane,
|
||||
.leaflet-shadow-pane,
|
||||
.leaflet-marker-pane,
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-overlay-pane svg,
|
||||
.leaflet-zoom-box,
|
||||
.leaflet-image-layer,
|
||||
.leaflet-layer {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-container {
|
||||
overflow: hidden;
|
||||
-ms-touch-action: none;
|
||||
}
|
||||
.leaflet-tile,
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
.leaflet-marker-icon,
|
||||
.leaflet-marker-shadow {
|
||||
display: block;
|
||||
}
|
||||
/* map is broken in FF if you have max-width: 100% on tiles */
|
||||
.leaflet-container img {
|
||||
max-width: none !important;
|
||||
}
|
||||
/* stupid Android 2 doesn't understand "max-width: none" properly */
|
||||
.leaflet-container img.leaflet-image-layer {
|
||||
max-width: 15000px !important;
|
||||
}
|
||||
.leaflet-tile {
|
||||
filter: inherit;
|
||||
visibility: hidden;
|
||||
}
|
||||
.leaflet-tile-loaded {
|
||||
visibility: inherit;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.leaflet-tile-pane { z-index: 2; }
|
||||
.leaflet-objects-pane { z-index: 3; }
|
||||
.leaflet-overlay-pane { z-index: 4; }
|
||||
.leaflet-shadow-pane { z-index: 5; }
|
||||
.leaflet-marker-pane { z-index: 6; }
|
||||
.leaflet-popup-pane { z-index: 7; }
|
||||
|
||||
|
||||
/* control positioning */
|
||||
|
||||
.leaflet-control {
|
||||
position: relative;
|
||||
z-index: 7;
|
||||
pointer-events: auto;
|
||||
}
|
||||
.leaflet-top,
|
||||
.leaflet-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
.leaflet-top {
|
||||
top: 0;
|
||||
}
|
||||
.leaflet-right {
|
||||
right: 0;
|
||||
}
|
||||
.leaflet-bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
.leaflet-left {
|
||||
left: 0;
|
||||
}
|
||||
.leaflet-control {
|
||||
float: left;
|
||||
clear: both;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
float: right;
|
||||
}
|
||||
.leaflet-top .leaflet-control {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.leaflet-left .leaflet-control {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.leaflet-right .leaflet-control {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
|
||||
/* zoom and fade animations */
|
||||
|
||||
.leaflet-fade-anim .leaflet-tile,
|
||||
.leaflet-fade-anim .leaflet-popup {
|
||||
opacity: 0;
|
||||
-webkit-transition: opacity 0.2s linear;
|
||||
-moz-transition: opacity 0.2s linear;
|
||||
-o-transition: opacity 0.2s linear;
|
||||
transition: opacity 0.2s linear;
|
||||
}
|
||||
.leaflet-fade-anim .leaflet-tile-loaded,
|
||||
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-animated {
|
||||
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
-o-transition: -o-transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
|
||||
}
|
||||
.leaflet-zoom-anim .leaflet-tile,
|
||||
.leaflet-pan-anim .leaflet-tile,
|
||||
.leaflet-touching .leaflet-zoom-animated {
|
||||
-webkit-transition: none;
|
||||
-moz-transition: none;
|
||||
-o-transition: none;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.leaflet-zoom-anim .leaflet-zoom-hide {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* cursors */
|
||||
|
||||
.leaflet-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
.leaflet-container {
|
||||
cursor: -webkit-grab;
|
||||
cursor: -moz-grab;
|
||||
}
|
||||
.leaflet-popup-pane,
|
||||
.leaflet-control {
|
||||
cursor: auto;
|
||||
}
|
||||
.leaflet-dragging,
|
||||
.leaflet-dragging .leaflet-clickable,
|
||||
.leaflet-dragging .leaflet-container {
|
||||
cursor: move;
|
||||
cursor: -webkit-grabbing;
|
||||
cursor: -moz-grabbing;
|
||||
}
|
||||
|
||||
|
||||
/* visual tweaks */
|
||||
|
||||
.leaflet-container {
|
||||
background: #ddd;
|
||||
outline: 0;
|
||||
}
|
||||
.leaflet-container a {
|
||||
color: #0078A8;
|
||||
}
|
||||
.leaflet-container a.leaflet-active {
|
||||
outline: 2px solid orange;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
border: 2px dotted #05f;
|
||||
background: white;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
|
||||
/* general typography */
|
||||
.leaflet-container {
|
||||
font: 12px/1.5 "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
|
||||
/* general toolbar styles */
|
||||
|
||||
.leaflet-bar {
|
||||
box-shadow: 0 1px 7px rgba(0,0,0,0.65);
|
||||
-webkit-border-radius: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a {
|
||||
background-color: #fff;
|
||||
border-bottom: 1px solid #ccc;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
line-height: 26px;
|
||||
display: block;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.leaflet-bar a,
|
||||
.leaflet-control-layers-toggle {
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
display: block;
|
||||
}
|
||||
.leaflet-bar a:hover {
|
||||
background-color: #f4f4f4;
|
||||
}
|
||||
.leaflet-bar a:first-child {
|
||||
-webkit-border-top-left-radius: 4px;
|
||||
border-top-left-radius: 4px;
|
||||
-webkit-border-top-right-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.leaflet-bar a:last-child {
|
||||
-webkit-border-bottom-left-radius: 4px;
|
||||
border-bottom-left-radius: 4px;
|
||||
-webkit-border-bottom-right-radius: 4px;
|
||||
border-bottom-right-radius: 4px;
|
||||
border-bottom: none;
|
||||
}
|
||||
.leaflet-bar a.leaflet-disabled {
|
||||
cursor: default;
|
||||
background-color: #f4f4f4;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-bar {
|
||||
-webkit-border-radius: 10px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:first-child {
|
||||
-webkit-border-top-left-radius: 7px;
|
||||
border-top-left-radius: 7px;
|
||||
-webkit-border-top-right-radius: 7px;
|
||||
border-top-right-radius: 7px;
|
||||
}
|
||||
.leaflet-touch .leaflet-bar a:last-child {
|
||||
-webkit-border-bottom-left-radius: 7px;
|
||||
border-bottom-left-radius: 7px;
|
||||
-webkit-border-bottom-right-radius: 7px;
|
||||
border-bottom-right-radius: 7px;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
|
||||
/* zoom control */
|
||||
|
||||
.leaflet-control-zoom-in {
|
||||
font: bold 18px 'Lucida Console', Monaco, monospace;
|
||||
}
|
||||
.leaflet-control-zoom-out {
|
||||
font: bold 22px 'Lucida Console', Monaco, monospace;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-zoom-in {
|
||||
font-size: 22px;
|
||||
line-height: 30px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-zoom-out {
|
||||
font-size: 28px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
|
||||
/* layers control */
|
||||
|
||||
.leaflet-control-layers {
|
||||
box-shadow: 0 1px 7px rgba(0,0,0,0.4);
|
||||
background: #f8f8f9;
|
||||
-webkit-border-radius: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers.png);
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
.leaflet-retina .leaflet-control-layers-toggle {
|
||||
background-image: url(images/layers-2x.png);
|
||||
background-size: 26px 26px;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers-toggle {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
.leaflet-control-layers .leaflet-control-layers-list,
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
|
||||
display: none;
|
||||
}
|
||||
.leaflet-control-layers-expanded .leaflet-control-layers-list {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
.leaflet-control-layers-expanded {
|
||||
padding: 6px 10px 6px 6px;
|
||||
color: #333;
|
||||
background: #fff;
|
||||
}
|
||||
.leaflet-control-layers-selector {
|
||||
margin-top: 2px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
.leaflet-control-layers label {
|
||||
display: block;
|
||||
}
|
||||
.leaflet-control-layers-separator {
|
||||
height: 0;
|
||||
border-top: 1px solid #ddd;
|
||||
margin: 5px -10px 5px -6px;
|
||||
}
|
||||
|
||||
|
||||
/* attribution and scale controls */
|
||||
|
||||
.leaflet-container .leaflet-control-attribution {
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
box-shadow: 0 0 5px #bbb;
|
||||
margin: 0;
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-scale-line {
|
||||
padding: 0 5px;
|
||||
color: #333;
|
||||
}
|
||||
.leaflet-container .leaflet-control-attribution,
|
||||
.leaflet-container .leaflet-control-scale {
|
||||
font-size: 11px;
|
||||
}
|
||||
.leaflet-left .leaflet-control-scale {
|
||||
margin-left: 5px;
|
||||
}
|
||||
.leaflet-bottom .leaflet-control-scale {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.leaflet-control-scale-line {
|
||||
border: 2px solid #777;
|
||||
border-top: none;
|
||||
color: black;
|
||||
line-height: 1.1;
|
||||
padding: 2px 5px 1px;
|
||||
font-size: 11px;
|
||||
text-shadow: 1px 1px 1px #fff;
|
||||
background-color: rgba(255, 255, 255, 0.5);
|
||||
box-shadow: 0 -1px 5px rgba(0, 0, 0, 0.2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child) {
|
||||
border-top: 2px solid #777;
|
||||
border-bottom: none;
|
||||
margin-top: -2px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
|
||||
border-bottom: 2px solid #777;
|
||||
}
|
||||
|
||||
.leaflet-touch .leaflet-control-attribution,
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-control-zoom {
|
||||
box-shadow: none;
|
||||
}
|
||||
.leaflet-touch .leaflet-control-layers,
|
||||
.leaflet-touch .leaflet-control-zoom {
|
||||
border: 4px solid rgba(0,0,0,0.3);
|
||||
}
|
||||
|
||||
|
||||
/* popup */
|
||||
|
||||
.leaflet-popup {
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
padding: 1px;
|
||||
text-align: left;
|
||||
-webkit-border-radius: 12px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.leaflet-popup-content {
|
||||
margin: 13px 19px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.leaflet-popup-content p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
margin: 0 auto;
|
||||
width: 40px;
|
||||
height: 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.leaflet-popup-tip {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
padding: 1px;
|
||||
|
||||
margin: -10px auto 0;
|
||||
|
||||
-webkit-transform: rotate(45deg);
|
||||
-moz-transform: rotate(45deg);
|
||||
-ms-transform: rotate(45deg);
|
||||
-o-transform: rotate(45deg);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
||||
background: white;
|
||||
|
||||
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 4px 4px 0 0;
|
||||
text-align: center;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
font: 16px/14px Tahoma, Verdana, sans-serif;
|
||||
color: #c3c3c3;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
background: transparent;
|
||||
}
|
||||
.leaflet-container a.leaflet-popup-close-button:hover {
|
||||
color: #999;
|
||||
}
|
||||
.leaflet-popup-scrolled {
|
||||
overflow: auto;
|
||||
border-bottom: 1px solid #ddd;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
||||
/* div icon */
|
||||
|
||||
.leaflet-div-icon {
|
||||
background: #fff;
|
||||
border: 1px solid #666;
|
||||
}
|
||||
.leaflet-editing-icon {
|
||||
-webkit-border-radius: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
.leaflet-vml-shape {
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
}
|
||||
.lvml {
|
||||
behavior: url(#default#VML);
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.leaflet-control {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.leaflet-popup-tip {
|
||||
width: 21px;
|
||||
_width: 27px;
|
||||
margin: 0 auto;
|
||||
_margin-top: -3px;
|
||||
|
||||
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
|
||||
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
|
||||
}
|
||||
.leaflet-popup-tip-container {
|
||||
margin-top: -1px;
|
||||
}
|
||||
.leaflet-popup-content-wrapper, .leaflet-popup-tip {
|
||||
border: 1px solid #999;
|
||||
}
|
||||
.leaflet-popup-content-wrapper {
|
||||
zoom: 1;
|
||||
}
|
||||
|
||||
.leaflet-control-zoom,
|
||||
.leaflet-control-layers {
|
||||
border: 3px solid #999;
|
||||
}
|
||||
.leaflet-control-layers-toggle {
|
||||
}
|
||||
.leaflet-control-attribution,
|
||||
.leaflet-control-layers,
|
||||
.leaflet-control-scale-line {
|
||||
background: white;
|
||||
}
|
||||
.leaflet-zoom-box {
|
||||
filter: alpha(opacity=50);
|
||||
}
|
||||
.leaflet-control-attribution {
|
||||
border-top: 1px solid #bbb;
|
||||
border-left: 1px solid #bbb;
|
||||
}
|
||||
Executable
+8
File diff suppressed because one or more lines are too long
Executable
+75
@@ -0,0 +1,75 @@
|
||||
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
|
||||
-webkit-transition: -webkit-transform 0.2s ease-out, opacity 0.2s ease-in;
|
||||
-moz-transition: -moz-transform 0.2s ease-out, opacity 0.2s ease-in;
|
||||
-o-transition: -o-transform 0.2s ease-out, opacity 0.2s ease-in;
|
||||
transition: transform 0.2s ease-out, opacity 0.2s ease-in;
|
||||
}
|
||||
.marker-cluster-small {
|
||||
background-color: rgba(181, 226, 140, 0.6);
|
||||
}
|
||||
.marker-cluster-small div {
|
||||
background-color: rgba(110, 204, 57, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-medium {
|
||||
background-color: rgba(241, 211, 87, 0.6);
|
||||
}
|
||||
.marker-cluster-medium div {
|
||||
background-color: rgba(240, 194, 12, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster-large {
|
||||
background-color: rgba(253, 156, 115, 0.6);
|
||||
}
|
||||
.marker-cluster-large div {
|
||||
background-color: rgba(241, 128, 23, 0.6);
|
||||
}
|
||||
|
||||
.marker-cluster {
|
||||
background-clip: padding-box;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.marker-cluster div {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
|
||||
text-align: center;
|
||||
border-radius: 15px;
|
||||
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
|
||||
}
|
||||
.marker-cluster span {
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.leaflet-label {
|
||||
background: #1f1f1f;
|
||||
background-clip: padding-box;
|
||||
border-radius: 4px;
|
||||
border-style: solid;
|
||||
border-width: 0px;
|
||||
display: block;
|
||||
font-weight: 200;
|
||||
font-size: 11pt;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
z-index: 99999 !important;
|
||||
}
|
||||
|
||||
.leaflet-label:before {
|
||||
border-right: 6px solid black;
|
||||
border-right-color: inherit;
|
||||
border-top: 6px solid transparent;
|
||||
border-bottom: 6px solid transparent;
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: -10px;
|
||||
display: none;
|
||||
}
|
||||
Executable
+16
File diff suppressed because one or more lines are too long
Executable
+9
@@ -0,0 +1,9 @@
|
||||
<div ng-controller='bettermap' ng-init="init()">
|
||||
<span ng-show="panel.spyable" style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
<!-- This solution might work well for other panels that have trouble with heights -->
|
||||
<div style="padding-right:10px;padding-top:10px;height:{{panel.height|| row.height}};overflow:hidden">
|
||||
<div bettermap id='bettermap' params="{{panel}}" style="height:100%"></div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+223
@@ -0,0 +1,223 @@
|
||||
/*
|
||||
|
||||
## Better maps
|
||||
|
||||
### Parameters
|
||||
* size :: How many results to show, more results = slower
|
||||
* field :: field containing a 2 element array in the format [lon,lat]
|
||||
* tooltip :: field to extract the tool tip value from
|
||||
* spyable :: Show the 'eye' icon that reveals the last ES query
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'./leaflet/leaflet-src',
|
||||
'require',
|
||||
|
||||
'css!./leaflet/leaflet.css',
|
||||
'css!./leaflet/plugins.css'
|
||||
],
|
||||
function (angular, app, _, L, localRequire) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.bettermap', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('bettermap', function($scope, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{
|
||||
title: 'Queries',
|
||||
src: 'app/partials/querySelect.html'
|
||||
}
|
||||
],
|
||||
status : "Experimental",
|
||||
description : "Displays geo points in clustered groups on a map. The cavaet for this panel is"+
|
||||
" that, for better or worse, it does NOT use the terms facet and it <b>does</b> query "+
|
||||
"sequentially. This however means that it transfers more data and is generally heavier to"+
|
||||
" compute, while showing less actual data. If you have a time filter, it will attempt to"+
|
||||
" show to most recent points in your search, up to your defined limit"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
size : 1000,
|
||||
spyable : true,
|
||||
tooltip : "_id",
|
||||
field : null
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
$scope.requireContext = localRequire;
|
||||
|
||||
// inorder to use relative paths in require calls, require needs a context to run. Without
|
||||
// setting this property the paths would be relative to the app not this context/file.
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.$on('refresh',function(){
|
||||
$scope.get_data();
|
||||
});
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.get_data = function(segment,query_id) {
|
||||
$scope.require(['./leaflet/plugins'], function () {
|
||||
$scope.panel.error = false;
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(_.isUndefined($scope.panel.field)) {
|
||||
$scope.panel.error = "Please select a field that contains geo point in [lon,lat] format";
|
||||
return;
|
||||
}
|
||||
|
||||
// Determine the field to sort on
|
||||
var timeField = _.uniq(_.pluck(filterSrv.getByType('time'),'field'));
|
||||
if(timeField.length > 1) {
|
||||
$scope.panel.error = "Time field must be consistent amongst time filters";
|
||||
} else if(timeField.length === 0) {
|
||||
timeField = null;
|
||||
} else {
|
||||
timeField = timeField[0];
|
||||
}
|
||||
|
||||
var _segment = _.isUndefined(segment) ? 0 : segment;
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// This could probably be changed to a BoolFilter
|
||||
var boolQuery = $scope.ejs.BoolQuery();
|
||||
_.each($scope.panel.queries.ids,function(id) {
|
||||
boolQuery = boolQuery.should(querySrv.getEjsObj(id));
|
||||
});
|
||||
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices[_segment])
|
||||
.query($scope.ejs.FilteredQuery(
|
||||
boolQuery,
|
||||
filterSrv.getBoolFilter(filterSrv.ids).must($scope.ejs.ExistsFilter($scope.panel.field))
|
||||
))
|
||||
.fields([$scope.panel.field,$scope.panel.tooltip])
|
||||
.size($scope.panel.size);
|
||||
|
||||
if(!_.isNull(timeField)) {
|
||||
request = request.sort(timeField,'desc');
|
||||
}
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
|
||||
if(_segment === 0) {
|
||||
$scope.hits = 0;
|
||||
$scope.data = [];
|
||||
query_id = $scope.query_id = new Date().getTime();
|
||||
}
|
||||
|
||||
// Check for error and abort if found
|
||||
if(!(_.isUndefined(results.error))) {
|
||||
$scope.panel.error = $scope.parse_error(results.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that we're still on the same query, if not stop
|
||||
if($scope.query_id === query_id) {
|
||||
|
||||
// Keep only what we need for the set
|
||||
$scope.data = $scope.data.slice(0,$scope.panel.size).concat(_.map(results.hits.hits, function(hit) {
|
||||
return {
|
||||
coordinates : new L.LatLng(hit.fields[$scope.panel.field][1],hit.fields[$scope.panel.field][0]),
|
||||
tooltip : hit.fields[$scope.panel.tooltip]
|
||||
};
|
||||
}));
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.$emit('draw');
|
||||
|
||||
// Get $size results then stop querying
|
||||
if($scope.data.length < $scope.panel.size && _segment+1 < dashboard.indices.length) {
|
||||
$scope.get_data(_segment+1,$scope.query_id);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('bettermap', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem, attrs) {
|
||||
|
||||
elem.html('<center><img src="img/load_big.gif"></center>');
|
||||
|
||||
// Receive render events
|
||||
scope.$on('draw',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
scope.$on('render', function(){
|
||||
if(!_.isUndefined(map)) {
|
||||
map.invalidateSize();
|
||||
map.getPanes();
|
||||
}
|
||||
});
|
||||
|
||||
var map, layerGroup;
|
||||
|
||||
function render_panel() {
|
||||
scope.require(['./leaflet/plugins'], function () {
|
||||
scope.panelMeta.loading = false;
|
||||
|
||||
if(_.isUndefined(map)) {
|
||||
map = L.map(attrs.id, {
|
||||
scrollWheelZoom: false,
|
||||
center: [40, -86],
|
||||
zoom: 10
|
||||
});
|
||||
|
||||
L.tileLayer('http://{s}.tile.cloudmade.com/57cbb6ca8cac418dbb1a402586df4528/22677/256/{z}/{x}/{y}.png', {
|
||||
maxZoom: 18,
|
||||
minZoom: 2
|
||||
}).addTo(map);
|
||||
layerGroup = new L.MarkerClusterGroup({maxClusterRadius:30});
|
||||
} else {
|
||||
layerGroup.clearLayers();
|
||||
}
|
||||
|
||||
_.each(scope.data, function(p) {
|
||||
if(!_.isUndefined(p.tooltip) && p.tooltip !== '') {
|
||||
layerGroup.addLayer(L.marker(p.coordinates).bindLabel(p.tooltip));
|
||||
} else {
|
||||
layerGroup.addLayer(L.marker(p.coordinates));
|
||||
}
|
||||
});
|
||||
|
||||
layerGroup.addTo(map);
|
||||
|
||||
map.fitBounds(_.pluck(scope.data,'coordinates'));
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<h4>Add Panel to Column</h4>
|
||||
<select class="input-medium" ng-model="new_panel.type" ng-options="f for f in _.without(config.panel_names,'column')| stringSort" ng-change="reset_panel(new_panel.type);send_render();"></select>
|
||||
<small>Select Type</small>
|
||||
<div ng-show="!(_.isUndefined(new_panel.type))">
|
||||
<div column-edit panel="new_panel" config="config" row="row" dashboards="dashboards" type="new_panel.type"></div>
|
||||
<button ng-click="add_panel(new_panel); reset_panel();" class="btn btn-primary">Create Panel</button><br>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
<h4>Panels</h4>
|
||||
<table class="table table-condensed table-striped">
|
||||
<thead>
|
||||
<th>Title</th>
|
||||
<th>Type</th>
|
||||
<th>Height</th>
|
||||
<th>Delete</th>
|
||||
<th>Move</th>
|
||||
<th></th>
|
||||
<th>Hide</th>
|
||||
</thead>
|
||||
<tr ng-repeat="app in panel.panels">
|
||||
<td>{{app.title}}</td>
|
||||
<td>{{app.type}}</td>
|
||||
<td><input type="text" class="input-small" ng-model="app.height"></input></td>
|
||||
<td><i ng-click="panel.panels = _.without(panel.panels,app)" class="pointer icon-remove"></i></td>
|
||||
<td><i ng-click="_.move(panel.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
|
||||
<td><i ng-click="_.move(panel.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
|
||||
<td><input type="checkbox" ng-model="app.hide" ng-checked="app.hide"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
<div ng-controller="column" ng-init="init();">
|
||||
<!-- Panels -->
|
||||
<div ng-repeat="(name, panel) in panel.panels" ng-hide="panel.height == '0px' || panel.hide" class="row-fluid panel" style="min-height:{{panel.height}}; position:relative">
|
||||
<!-- Error Panel -->
|
||||
<div class="row-fluid">
|
||||
<div class="span12 alert alert-error panel-error" ng-hide="!panel.error">
|
||||
<a class="close" ng-click="panel.error=false">×</a>
|
||||
<i class="icon-exclamation-sign"></i> <strong>Oops!</strong> {{panel.error}}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Content Panel -->
|
||||
<div class="row-fluid">
|
||||
<kibana-panel type="panel.type"></kibana-panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
|
||||
## Column
|
||||
|
||||
### Parameters
|
||||
* panels :: an array of panel objects. All of their spans should be set to 12
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'config'
|
||||
],
|
||||
function (angular, app, _, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.column', []);
|
||||
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('column', function($scope, $rootScope, $timeout) {
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "A pseudo panel that lets you add other panels to be arranged in a column with"+
|
||||
"defined heights."
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
panels : []
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function(){
|
||||
$scope.reset_panel();
|
||||
};
|
||||
|
||||
$scope.toggle_row = function(panel) {
|
||||
panel.collapse = panel.collapse ? false : true;
|
||||
if (!panel.collapse) {
|
||||
$timeout(function() {
|
||||
$scope.send_render();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.send_render = function() {
|
||||
$scope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.add_panel = function(panel) {
|
||||
$scope.panel.panels.push(panel);
|
||||
};
|
||||
|
||||
$scope.reset_panel = function(type) {
|
||||
$scope.new_panel = {
|
||||
loading: false,
|
||||
error: false,
|
||||
sizeable: false,
|
||||
span: 12,
|
||||
height: "150px",
|
||||
editable: true,
|
||||
type: type,
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('columnEdit', function($compile,$timeout) {
|
||||
return {
|
||||
scope : {
|
||||
new_panel:"=panel",
|
||||
row:"=",
|
||||
config:"=",
|
||||
dashboards:"=",
|
||||
type:"=type"
|
||||
},
|
||||
link: function(scope, elem) {
|
||||
scope.$on('render', function () {
|
||||
|
||||
// Make sure the digest has completed and populated the attributes
|
||||
$timeout(function() {
|
||||
// Create a reference to the new_panel as panel so that the existing
|
||||
// editors work with our isolate scope
|
||||
scope.panel = scope.new_panel;
|
||||
var template = '<div ng-include src="partial(\'panelgeneral\')"></div>';
|
||||
|
||||
if(!(_.isUndefined(scope.type)) && scope.type !== "") {
|
||||
template = template+'<div ng-include src="\'app/panels/'+scope.type+'/editor.html\'"></div>';
|
||||
}
|
||||
elem.html($compile(angular.element(template))(scope));
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('withoutColumn', function() {
|
||||
return function() {
|
||||
return _.without(config.panel_names,'column');
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Height</label> <input type="text" class="input-mini" ng-model='panel.height'></input>
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small"> Editable </label><input type="checkbox" ng-model="panel.editable" ng-checked="panel.editable">
|
||||
</div>
|
||||
</div>
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
<div>
|
||||
<h5>Allow saving to</h5>
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Export</label><input type="checkbox" ng-model="panel.save.local" ng-checked="panel.save.local">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Defaults</label><input type="checkbox" ng-model="panel.save.default" ng-checked="panel.save.default">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Gist <tip>Requires your domain to be OAUTH registered with Github<tip></label><input type="checkbox" ng-model="panel.save.gist" ng-checked="panel.save.gist">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="panel.save.elasticsearch" ng-checked="panel.save.elasticsearch">
|
||||
</div>
|
||||
</div>
|
||||
<h5>Allow loading from</h5>
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Local file</label><input type="checkbox" ng-model="panel.load.local" ng-checked="panel.load.local">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Gist</label><input type="checkbox" ng-model="panel.load.gist" ng-checked="panel.load.gist">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Elasticsearch</label><input type="checkbox" ng-model="panel.load.elasticsearch" ng-checked="panel.load.elasticsearch">
|
||||
</div>
|
||||
<div class="span3" ng-show="panel.load.elasticsearch">
|
||||
<label class="small">ES list size</label><input class="input-mini" type="number" ng-model="panel.elasticsearch_size">
|
||||
</div>
|
||||
</div>
|
||||
<h5>Sharing</h5>
|
||||
<div class="row-fluid">
|
||||
<div class="span2" >
|
||||
<label class="small">Allow Sharing</label><input type="checkbox" ng-model="panel.temp" ng-checked="panel.temp">
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.temp">
|
||||
<label class="small">TTL</label><input type="checkbox" ng-model="panel.ttl_enable" ng-checked="panel.temp">
|
||||
</div>
|
||||
<div class="span5" ng-show="panel.temp && panel.ttl_enable">
|
||||
<label class="small">TTL Duration <i class="icon-question-sign" bs-tooltip="'Elasticsearch date math, eg: 1m,1d,1w,30d'"></i></label><input class="input-small" type="text" ng-model="panel.temp_ttl">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+40
@@ -0,0 +1,40 @@
|
||||
<div>
|
||||
<a class="close" ng-click="dismiss()" href="">×</a>
|
||||
<h4>Load</h4>
|
||||
<div ng-show='panel.load.local'>
|
||||
<h5>Local File</h5>
|
||||
<form>
|
||||
<input type="file" id="dashupload" dash-upload /><br>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show='panel.load.gist'>
|
||||
<h5>Gist <small>Enter a gist number or url</small></h5>
|
||||
<form>
|
||||
<input type="text" ng-model="gist.url"/><br>
|
||||
<button class="btn" ng-click="gist_dblist(dashboard.gist_id(gist.url))" ng-show="dashboard.is_gist(gist.url)"><i class="icon-github-alt"></i> Get gist:{{gist.url | gistid}}</button>
|
||||
<h6 ng-show="gist.files.length">Dashboards in gist:{{gist.url | gistid}} <small>click to load</small></h6>
|
||||
<h6 ng-hide="gist.files.length">No gist dashboards found</h6>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr ng-repeat="file in gist.files">
|
||||
<td><a ng-click="dashboard.dash_load(file)">{{file.title}}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show='panel.load.elasticsearch'>
|
||||
<h5>Elasticsearch</h5>
|
||||
<form class="input-append">
|
||||
<input type="text" ng-model="elasticsearch.query"/>
|
||||
<button ng-click="elasticsearch_dblist(elasticsearch.query)" class='btn'><i class='icon-search'></i></button>
|
||||
</form>
|
||||
<h6 ng-show="elasticsearch.dashboards.length">Elasticsearch stored dashboards</h6>
|
||||
<h6 ng-hide="elasticsearch.dashboards.length">No dashboards matching your query found</h6>
|
||||
<table class="table table-condensed table-striped">
|
||||
<tr ng-repeat="row in elasticsearch.dashboards | orderBy:['_id']">
|
||||
<td><a ng-click="elasticsearch_delete(row._id)"><i class="icon-remove"></i></a></td>
|
||||
<td><a href="#/dashboard/elasticsearch/{{row._id}}">{{row._id}}</a></td>
|
||||
<td><a><i class="icon-share" ng-click="share = dashboard.share_link(row._id,'elasticsearch',row._id)" bs-modal="'app/panels/dashcontrol/share.html'"></i></a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+6
@@ -0,0 +1,6 @@
|
||||
<div ng-controller='dashcontrol' ng-init="init()">
|
||||
<label class='small'>Dash Control <tip icon="warning-sign">This panel is deprecated! Please remove it from your dashboard</tip></label>
|
||||
<button class='btn' ng-show="panel.load.gist || panel.load.elasticsearch || panel.load.local" data-placement="bottom" data-unique="1" ng-click="elasticsearch_dblist(elasticsearch.query)" bs-popover="'app/panels/dashcontrol/load.html'"><i class='icon-folder-open'></i> <i class='icon-caret-down'></i></button>
|
||||
<button class='btn' ng-show="panel.save.gist || panel.save.elasticsearch || panel.save.local || panel.save.default" data-placement="bottom" data-unique="1" bs-popover="'app/panels/dashcontrol/save.html'"><i class='icon-save'></i> <i class='icon-caret-down'></i></button>
|
||||
<button ng-show="panel.temp" class='btn' ng-click="elasticsearch_save('temp',panel.temp_ttl)" bs-modal="'app/panels/dashcontrol/share.html'"><i class='icon-share'></i></button>
|
||||
</div>
|
||||
Executable
+198
@@ -0,0 +1,198 @@
|
||||
/*
|
||||
## Dashcontrol
|
||||
|
||||
### Parameters
|
||||
* save
|
||||
** gist :: Allow saving to gist. Requires registering an oauth domain with Github
|
||||
** elasticsearch :: Allow saving to a special Kibana index within Elasticsearch
|
||||
** local :: Allow saving to local file
|
||||
* load
|
||||
** gist :: Allow loading from gists
|
||||
** elasticsearch :: Allow searching and loading of elasticsearch saved dashboards
|
||||
** local :: Allow loading of dashboards from Elasticsearch
|
||||
* hide_control :: Upon save, hide this panel
|
||||
* elasticsearch_size :: show this many dashboards under the ES section in the load drop down
|
||||
* temp :: Allow saving of temp dashboards
|
||||
* ttl :: Enable setting ttl.
|
||||
* temp_ttl :: How long should temp dashboards persist
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function(angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.dashcontrol', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('dashcontrol', function($scope, $http, timer, dashboard, alertSrv) {
|
||||
$scope.panelMeta = {
|
||||
status : "Deprecated",
|
||||
description : "This panel has been moved to the navigation bar. See the dashboard setting editor to configure it."
|
||||
};
|
||||
|
||||
$scope.panel = $scope.panel || {};
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
save : {
|
||||
gist: false,
|
||||
elasticsearch: true,
|
||||
local: true,
|
||||
'default': true
|
||||
},
|
||||
load : {
|
||||
gist: true,
|
||||
elasticsearch: true,
|
||||
local: true
|
||||
},
|
||||
hide_control: false,
|
||||
elasticsearch_size: 20,
|
||||
temp: true,
|
||||
ttl_enable: true,
|
||||
temp_ttl: '30d'
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
$scope.gist = {};
|
||||
$scope.elasticsearch = {};
|
||||
};
|
||||
|
||||
$scope.set_default = function() {
|
||||
if(dashboard.set_default()) {
|
||||
alertSrv.set('Local Default Set',dashboard.current.title+' has been set as your local default','success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.purge_default = function() {
|
||||
if(dashboard.purge_default()) {
|
||||
alertSrv.set('Local Default Clear','Your local default dashboard has been cleared','success',5000);
|
||||
} else {
|
||||
alertSrv.set('Incompatible Browser','Sorry, your browser is too old for this feature','error',5000);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.elasticsearch_save = function(type,ttl) {
|
||||
dashboard.elasticsearch_save(
|
||||
type,
|
||||
($scope.elasticsearch.title || dashboard.current.title),
|
||||
($scope.panel.ttl_enable ? ttl : false)
|
||||
).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result._id)) {
|
||||
alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' +
|
||||
result._id + '"','success',5000);
|
||||
if(type === 'temp') {
|
||||
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
|
||||
}
|
||||
} else {
|
||||
alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.elasticsearch_delete = function(id) {
|
||||
dashboard.elasticsearch_delete(id).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result)) {
|
||||
if(result.found) {
|
||||
alertSrv.set('Dashboard Deleted',id+' has been deleted','success',5000);
|
||||
// Find the deleted dashboard in the cached list and remove it
|
||||
var toDelete = _.where($scope.elasticsearch.dashboards,{_id:id})[0];
|
||||
$scope.elasticsearch.dashboards = _.without($scope.elasticsearch.dashboards,toDelete);
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Found','Could not find '+id+' in Elasticsearch','warning',5000);
|
||||
}
|
||||
} else {
|
||||
alertSrv.set('Dashboard Not Deleted','An error occurred deleting the dashboard','error',5000);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.elasticsearch_dblist = function(query) {
|
||||
dashboard.elasticsearch_list(query,$scope.panel.elasticsearch_size).then(
|
||||
function(result) {
|
||||
if(!_.isUndefined(result.hits)) {
|
||||
$scope.panel.error = false;
|
||||
$scope.hits = result.hits.total;
|
||||
$scope.elasticsearch.dashboards = result.hits.hits;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.save_gist = function() {
|
||||
dashboard.save_gist($scope.gist.title).then(
|
||||
function(link) {
|
||||
if(!_.isUndefined(link)) {
|
||||
$scope.gist.last = link;
|
||||
alertSrv.set('Gist saved','You will be able to access your exported dashboard file at '+
|
||||
'<a href="'+link+'">'+link+'</a> in a moment','success');
|
||||
} else {
|
||||
alertSrv.set('Save failed','Gist could not be saved','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.gist_dblist = function(id) {
|
||||
dashboard.gist_list(id).then(
|
||||
function(files) {
|
||||
if(files && files.length > 0) {
|
||||
$scope.gist.files = files;
|
||||
} else {
|
||||
alertSrv.set('Gist Failed','Could not retrieve dashboard list from gist','error',5000);
|
||||
}
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('dashUpload', function(timer, dashboard, alertSrv){
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope) {
|
||||
function file_selected(evt) {
|
||||
var files = evt.target.files; // FileList object
|
||||
|
||||
// unused.. var output = []; // files is a FileList of File objects. List some properties.
|
||||
var readerOnload = function() {
|
||||
return function(e) {
|
||||
dashboard.dash_load(JSON.parse(e.target.result));
|
||||
scope.$apply();
|
||||
};
|
||||
};
|
||||
for (var i = 0, f; f = files[i]; i++) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = (readerOnload)(f);
|
||||
reader.readAsText(f);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for the various File API support.
|
||||
if (window.File && window.FileReader && window.FileList && window.Blob) {
|
||||
// Something
|
||||
document.getElementById('dashupload').addEventListener('change', file_selected, false);
|
||||
} else {
|
||||
alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('gistid', function() {
|
||||
var gist_pattern = /(\d{5,})|([a-z0-9]{10,})|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
|
||||
return function(input) {
|
||||
//return input+"boners"
|
||||
if(!(_.isUndefined(input))) {
|
||||
var output = input.match(gist_pattern);
|
||||
if(!_.isNull(output) && !_.isUndefined(output)) {
|
||||
return output[0].replace(/.*\//, '');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
<div>
|
||||
<a class="close" ng-click="dismiss()" href="">×</a>
|
||||
<h4>Save</h4>
|
||||
|
||||
<div ng-show="panel.save.default || panel.save.local">
|
||||
<h5>Locally</h5>
|
||||
<form>
|
||||
<ul class="nav nav-list">
|
||||
<li><a ng-show="panel.save.local" ng-click="dashboard.to_file()"><i class="icon-download"></i> Export to File</a></li>
|
||||
<li><a ng-show="panel.save.default" ng-click="set_default()"><i class="icon-bookmark"></i> Set as My Default</a></li>
|
||||
<li><a ng-show="panel.save.default" ng-click="purge_default()"><i class="icon-ban-circle"></i> Clear My Default</a></li>
|
||||
</ul>
|
||||
</form>
|
||||
</div>
|
||||
<div ng-show="panel.save.gist">
|
||||
<h5>Gist</h5>
|
||||
<form class="input-append">
|
||||
<input class='input-medium' placeholder='Title' type="text" ng-model="gist.title"/>
|
||||
<button class="btn" ng-click="save_gist()"><i class="icon-github-alt"></i></button>
|
||||
</form><br>
|
||||
<small ng-show="gist.last">Last gist: <a target="_blank" href="{{gist.last}}">{{gist.last}}</a></small>
|
||||
</div>
|
||||
<div ng-show="panel.save.elasticsearch">
|
||||
<h5>Elasticsearch</h5>
|
||||
<form class="input-append">
|
||||
<input class='input-medium' placeholder='Title' type="text" ng-model="elasticsearch.title"/>
|
||||
<button class="btn" ng-click="elasticsearch_save('dashboard')"><i class="icon-save"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h3>{{share.title}} <small>shareable link</small></h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<label>Share this dashboard with this URL</label>
|
||||
<input ng-model='share.link' type="text" style="width:90%" onclick="this.select()" onfocus="this.select()" ng-change="share = dashboard.share_link(share.title,share.type,share.id)">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success" ng-click="dismiss();$broadcast('render')">Close</button>
|
||||
</div>
|
||||
Executable
+23
@@ -0,0 +1,23 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span1">
|
||||
<label class="small">Length</label>
|
||||
<input type="number" style="width:80%" ng-model="panel.size" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Field</label>
|
||||
<input type="text" bs-typeahead="fields.list" style="width:80%" ng-change="set_refresh(true)" ng-model='panel.field'></select>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Query Mode</label>
|
||||
<select style="width:80%" ng-change="set_refresh(true)" ng-model='panel.mode' ng-options="f for f in ['terms only','AND', 'OR']"></select>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<label class="small">Exclude Terms(s) (comma seperated)</label>
|
||||
<input array-join type="text" style="width:90%" ng-change="set_refresh(true)" ng-model='panel.exclude'></input>
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small"> Rest </label><input type="checkbox" ng-model="panel.rest" ng-checked="panel.rest" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
<div ng-controller='derivequeries' ng-init="init()">
|
||||
<style>
|
||||
.end-derive {
|
||||
position:absolute;
|
||||
right:15px;
|
||||
top:5px;
|
||||
}
|
||||
.panel-derive-field {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.panel-derive {
|
||||
padding-right: 35px !important;
|
||||
height: 31px !important;
|
||||
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box; /* Firefox, other Gecko */
|
||||
box-sizing: border-box; /* Opera/IE 8+ */
|
||||
}
|
||||
</style>
|
||||
<span ng-show='panel.spyable' style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
|
||||
<label class="small">Create new queries from
|
||||
<span class="panel-derive-field" ng-show="!editing" ng-click="editing=true">{{panel.field}}</span>
|
||||
<select ng-show="editing && fields.list.length>1" class="input-medium" ng-model="panel.field" ng-options="f for f in fields.list" ng-change='editing=false' ng-blur="editing=false"></select>
|
||||
<input ng-show="editing && fields.list.length<2" type="text" ng-model="panel.field" ng-blur="editing=false"/>
|
||||
({{panel.mode}} mode)</label>
|
||||
<div>
|
||||
<form class="form-search" style="position:relative" ng-submit="get_data()">
|
||||
<input class="search-query panel-derive input-block-level" bs-typeahead="panel.history" data-min-length=0 data-items=100 type="text" ng-model="panel.query"/>
|
||||
<span class="end-derive">
|
||||
<i class="icon-search pointer" ng-click="get_data()"></i>
|
||||
</span
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+152
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
## Derivequeries
|
||||
|
||||
### Parameters
|
||||
* label :: The label to stick over the field
|
||||
* query :: A string to use as a filter for the terms facet
|
||||
* field :: the field to facet on
|
||||
* rest :: include a filter that matches all other terms,
|
||||
* size :: how many queries to generate
|
||||
* fields :: a list of fields known to us
|
||||
* query_mode :: how to create query
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.derivequeries', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('derivequeries', function($scope, $rootScope, querySrv, fields, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
status : "Experimental",
|
||||
description : "Creates a new set of queries using the Elasticsearch terms facet. For example,"+
|
||||
" you might want to create 5 queries showing the most frequent HTTP response codes. Be "+
|
||||
"careful not to select a high cardinality field, as Elasticsearch must load all unique values"+
|
||||
" into memory."
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
loading : false,
|
||||
label : "Search",
|
||||
query : "*",
|
||||
ids : [],
|
||||
field : '_type',
|
||||
fields : [],
|
||||
spyable : true,
|
||||
rest : false,
|
||||
size : 5,
|
||||
mode : 'terms only',
|
||||
exclude : [],
|
||||
history : [],
|
||||
remember: 10 // max: 100, angular strap can't take a variable for items param
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.editing = false;
|
||||
$scope.panel.fields = fields.list;
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
update_history($scope.panel.query);
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices);
|
||||
|
||||
// Terms mode
|
||||
request = request
|
||||
.facet($scope.ejs.TermsFacet('query')
|
||||
.field($scope.panel.field)
|
||||
.size($scope.panel.size)
|
||||
.exclude($scope.panel.exclude)
|
||||
.facetFilter($scope.ejs.QueryFilter(
|
||||
$scope.ejs.FilteredQuery(
|
||||
$scope.ejs.QueryStringQuery($scope.panel.query || '*'),
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
)))).size(0);
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
var suffix;
|
||||
if ($scope.panel.query === '' || $scope.panel.mode === 'terms only') {
|
||||
suffix = '';
|
||||
} else if ($scope.panel.mode === 'AND') {
|
||||
suffix = ' AND (' + $scope.panel.query + ')';
|
||||
} else if ($scope.panel.mode === 'OR') {
|
||||
suffix = ' OR (' + $scope.panel.query + ')';
|
||||
}
|
||||
var ids = [];
|
||||
var terms = results.facets.query.terms;
|
||||
var others = [];
|
||||
_.each(terms, function(v) {
|
||||
var _q = $scope.panel.field+':"'+v.term+'"'+suffix;
|
||||
// if it isn't in the list, remove it
|
||||
var _iq = querySrv.findQuery(_q);
|
||||
if(!_iq) {
|
||||
ids.push(querySrv.set({alias: v.term, query:_q}));
|
||||
} else {
|
||||
ids.push(_iq.id);
|
||||
}
|
||||
others.push("NOT (" + _q + ")");
|
||||
});
|
||||
if ($scope.panel.rest) {
|
||||
var _other_q = others.join(' AND ');
|
||||
var _iq = querySrv.findQuery(_other_q);
|
||||
if (!_iq) {
|
||||
ids.push(querySrv.set({alias: 'other', query: _other_q}));
|
||||
} else {
|
||||
ids.push(_iq.id);
|
||||
}
|
||||
}
|
||||
_.each(_.difference($scope.panel.ids,ids),function(id){
|
||||
querySrv.remove(id);
|
||||
});
|
||||
$scope.panel.ids = ids;
|
||||
dashboard.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
};
|
||||
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
};
|
||||
|
||||
var update_history = function(query) {
|
||||
query = _.isArray(query) ? query : [query];
|
||||
if($scope.panel.remember > 0) {
|
||||
$scope.panel.history = _.union(query.reverse(),$scope.panel.history);
|
||||
var _length = $scope.panel.history.length;
|
||||
if(_length > $scope.panel.remember) {
|
||||
$scope.panel.history = $scope.panel.history.slice(0,$scope.panel.remember);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span3"><h6>Popup Position</h6>
|
||||
<select class="input-small" ng-model="panel.micropanel_position" ng-options="f for f in ['top','right','bottom','left']" ng-change="reload_list();"></select></span>
|
||||
</div>
|
||||
<div class="span3"><h6>List Arrangement</h6>
|
||||
<select class="input-small" ng-model="panel.arrange" ng-options="f for f in ['horizontal','vertical']"></select></span>
|
||||
</div>
|
||||
<div class="span3"><h6>Font Size</h6>
|
||||
<select class="input-small" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
|
||||
</div>
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
<a class="close" ng-click="dismiss()" href="">×</a>
|
||||
<h4>
|
||||
Micro Analysis of {{micropanel.field}}
|
||||
<i class="pointer icon-search" ng-click="fieldExists(micropanel.field,'must');dismiss();"></i>
|
||||
<i class="pointer icon-ban-circle" ng-click="fieldExists(micropanel.field,'mustNot');dismiss();"></i>
|
||||
<br><small>{{micropanel.count}} events in the table set</small>
|
||||
</h4>
|
||||
<table style="width:480px" class='table table-bordered table-striped table-condensed'>
|
||||
<thead>
|
||||
<th>{{micropanel.field}}</th>
|
||||
<th>Action</th>
|
||||
<th>In set</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat='field in micropanel.values'>
|
||||
<td>{{{true: "__blank__",false:field[0]}[field[0] == ""]}}</td>
|
||||
<td>
|
||||
<i class="pointer icon-search" ng-click="build_search(micropanel.field,field[0],'must');dismiss();"></i>
|
||||
<i class="pointer icon-ban-circle" ng-click="build_search(micropanel.field,field[0],'mustNot');dismiss();"></i>
|
||||
</td>
|
||||
<td>{{field[1]}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span ng-repeat='(field,count) in micropanel.related'><a ng-click="toggle_field(field)">{{field}}</a> ({{Math.round((count / micropanel.count) * 100)}}%),</span>
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
<div ng-controller='fields' ng-init="init()">
|
||||
<h4>The 'fields' panel is deprecated.</h4> The table panel now integrates a field selector.
|
||||
</div>
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
## Fields (DEPRECATED)
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.fields', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('fields', function($scope) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Deprecated",
|
||||
description : "You should not use this table, it does not work anymore. The table panel now"+
|
||||
"integrates a field selector. This module will soon be removed."
|
||||
};
|
||||
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
style : {},
|
||||
arrange : 'vertical',
|
||||
micropanel_position : 'right',
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
// Place holder until I remove this
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
No options here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+15
@@ -0,0 +1,15 @@
|
||||
<div>
|
||||
<style>
|
||||
.input-query-alias {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
</style>
|
||||
<a class="close" ng-click="render();dismiss();" href="">×</a>
|
||||
<h6>Query Alias</h6>
|
||||
<form>
|
||||
<input class="input-medium input-query-alias" type="text" ng-model="queries.list[id].alias" placeholder='Alias...' />
|
||||
<div>
|
||||
<i ng-repeat="color in queries.colors" class="pointer" ng-class="{'icon-circle-blank':queries.list[id].color == color,'icon-circle':queries.list[id].color != color}" style="color:{{color}}" ng-click="queries.list[id].color = color;render();"> </i>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
<div ng-controller='filtering' ng-init="init()">
|
||||
<style>
|
||||
.filtering-container {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.filter-panel-filter {
|
||||
display:inline-block;
|
||||
vertical-align: top;
|
||||
margin-left: 10px;
|
||||
width: 220px;
|
||||
padding: 5px 5px 0px 5px;
|
||||
border: #555 1px solid;
|
||||
margin: 0px 5px 5px 0px;
|
||||
}
|
||||
.filter-panel-filter ul {
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
.filter-must {
|
||||
border-bottom: #7EB26D 3px solid;
|
||||
}
|
||||
.filter-mustNot {
|
||||
border-bottom: #E24D42 3px solid;
|
||||
}
|
||||
.filter-deselected {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.filter-either {
|
||||
border-bottom: #EF843C 3px solid;
|
||||
}
|
||||
.filter-action {
|
||||
float:right;
|
||||
margin-bottom: 0px !important;
|
||||
margin-left: 3px;
|
||||
}
|
||||
.filter-mandate {
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
.filter-apply {
|
||||
float:right;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class='filtering-container'>
|
||||
<span ng-show="filterSrv.ids.length == 0"><h5>No filters available</h5></span>
|
||||
<div ng-repeat="id in filterSrv.ids" class="small filter-panel-filter" ng-class="{'filter-deselected': !filterSrv.list[id].active}">
|
||||
<div class="filter-{{filterSrv.list[id].mandate}}" >
|
||||
<strong>{{filterSrv.list[id].type}}</strong>
|
||||
<span ng-show="!filterSrv.list[id].editing" class="filter-mandate" ng-click="filterSrv.list[id].editing = true">{{filterSrv.list[id].mandate}}</span>
|
||||
|
||||
<span ng-show="filterSrv.list[id].editing">
|
||||
<select class="input-small" ng-model="filterSrv.list[id].mandate" ng-options="f for f in ['must','mustNot','either']"></select>
|
||||
</span>
|
||||
|
||||
<i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(id)"></i>
|
||||
<i class="filter-action pointer" ng-class="{'icon-check': filterSrv.list[id].active,'icon-check-empty': !filterSrv.list[id].active}" bs-tooltip="'Toggle'" ng-click="toggle(id)"></i>
|
||||
<i class="filter-action pointer icon-edit" ng-hide="filterSrv.list[id].editing" bs-tooltip="'Edit'" ng-click="filterSrv.list[id].editing = true"></i>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-hide="filterSrv.list[id].editing && isEditable(filterSrv.list[id])">
|
||||
<ul class="unstyled">
|
||||
<li ng-repeat="(key,value) in filterSrv.list[id]" ng-show="show_key(key)"><strong>{{key}}</strong> : {{value}}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div ng-show="filterSrv.list[id].editing && isEditable(filterSrv.list[id])">
|
||||
<ul class="unstyled">
|
||||
<li ng-repeat="key in _.keys(filterSrv.list[id])" ng-show="show_key(key)"><strong>{{key}}</strong> : <input type='text' ng-model="filterSrv.list[id][key]"></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="filter-apply" ng-show="filterSrv.list[id].editing">
|
||||
<button ng-click="filterSrv.list[id].editing=undefined" class="btn btn-mini" bs-tooltip="'Save without refresh'">Save</button>
|
||||
<button ng-click="filterSrv.list[id].editing=undefined;refresh()" class="btn btn-success btn-mini" bs-tooltip="'Save and refresh'">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
|
||||
## filtering
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore'
|
||||
],
|
||||
function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.filtering', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('filtering', function($scope, filterSrv, $rootScope, dashboard) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Beta",
|
||||
description : "A controllable list of all filters currently applied to the dashboard. You "+
|
||||
"almost certainly want one of these on your dashboard somewhere."
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.filterSrv = filterSrv;
|
||||
};
|
||||
|
||||
$scope.remove = function(id) {
|
||||
filterSrv.remove(id);
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
$scope.toggle = function(id) {
|
||||
filterSrv.list[id].active = !filterSrv.list[id].active;
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
$rootScope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.show_key = function(key) {
|
||||
return !_.contains(['type','id','alias','mandate','active','editing'],key);
|
||||
};
|
||||
|
||||
$scope.isEditable = function(filter) {
|
||||
var uneditable = ['time'];
|
||||
if(_.contains(uneditable,filter.type)) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
});
|
||||
});
|
||||
Executable
+72
@@ -0,0 +1,72 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Mode</label>
|
||||
<select ng-change="set_refresh(true)" class="input-small" ng-model="panel.mode" ng-options="f for f in ['count','min','mean','max','total']"></select>
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Time Field</label>
|
||||
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.time_field">
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.mode != 'count'">
|
||||
<label class="small">Value Field</label>
|
||||
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.value_field">
|
||||
</div>
|
||||
<div class="span3" ng-show="panel.mode != 'count'">
|
||||
<label class="small">Note</label><small> In <strong>{{panel.mode}}</strong> mode the configured field <strong>must</strong> be a numeric type</small>
|
||||
</div>
|
||||
</div>
|
||||
<h5>Chart Settings</h5>
|
||||
<div class="row-fluid" style="margin-bottom:10px;">
|
||||
<div class="span1"> <label class="small">Bars</label><input type="checkbox" ng-model="panel.bars" ng-checked="panel.bars"></div>
|
||||
<div class="span1"> <label class="small">Lines</label><input type="checkbox" ng-model="panel.lines" ng-checked="panel.lines"></div>
|
||||
<div class="span1"> <label class="small">Points</label><input type="checkbox" ng-model="panel.points" ng-checked="panel.points"></div>
|
||||
<div class="span1"> <label class="small">Stack</label><input type="checkbox" ng-model="panel.stack" ng-checked="panel.stack"></div>
|
||||
<!--<div class="span1" ng-show="panel.stack">Percent <tip>Stack as a percentage of total</tip></label><input type="checkbox" ng-model="panel.percentage" ng-checked="panel.percentage"></div>-->
|
||||
<div class="span1"> <label class="small">Legend</label><input type="checkbox" ng-model="panel.legend" ng-checked="panel.legend"></div>
|
||||
<div class="span1"> <label class="small">xAxis</label><input type="checkbox" ng-model="panel['x-axis']" ng-checked="panel['x-axis']"></div>
|
||||
<div class="span1"> <label class="small">yAxis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']"></div>
|
||||
<div class="span2" ng-show="panel.lines">
|
||||
<label class="small">Line Fill</label>
|
||||
<select class="input-mini" ng-model="panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"></select>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.lines">
|
||||
<label class="small">Line Width</label>
|
||||
<select class="input-mini" ng-model="panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Time correction</label>
|
||||
<select ng-model="panel.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
|
||||
</div>
|
||||
<div class="span1"> <label class="small">Selectable</label><input type="checkbox" ng-model="panel.interactive" ng-checked="panel.interactive"></div>
|
||||
<div class="span2">
|
||||
<label class="small">Zoom Links</label><input type="checkbox" ng-model="panel.zoomlinks" ng-checked="panel.zoomlinks" />
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Auto-interval</label><input type="checkbox" ng-model="panel.auto_int" ng-checked="panel.auto_int" />
|
||||
</div>
|
||||
<div class="span2" ng-show='panel.auto_int'>
|
||||
<label class="small">Resolution</label><input type="number" class='input-mini' ng-model="panel.resolution" ng-change='set_refresh(true)'/>
|
||||
</div>
|
||||
<div class="span3" ng-show='panel.auto_int'>
|
||||
<label class="small">Shoot for this many data points, rounding to sane intervals</label>
|
||||
</div>
|
||||
<div class="span2" ng-hide='panel.auto_int'>
|
||||
<label class="small">Interval</label><input type="text" class='input-mini' ng-model="panel.interval" ng-change='set_refresh(true)'/>
|
||||
</div>
|
||||
<div class="span3" ng-hide='panel.auto_int'>
|
||||
<label class="small">Use Elasticsearch date math format (eg 1m, 5m, 1d, 2w, 1y)</label>
|
||||
</div>
|
||||
</div>
|
||||
<h5>Tooltip Settings</h5>
|
||||
<div class="row-fluid" style="margin-bottom:10px;">
|
||||
<div class="span3">
|
||||
<label class="small">Stacked Values <tip>How should the values in stacked charts to be calculated?</tip></label>
|
||||
<select class="input-medium" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']"></select>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Display Query <tip>If an alias is set, it will be shown in the tooltip. If not, should it show the query?</tip></label>
|
||||
<input type="checkbox" ng-model="panel.tooltip.query_as_alias" />
|
||||
</div>
|
||||
</div>
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<div ng-controller='histogram' ng-init="init()" style="height:{{panel.height || row.height}}">
|
||||
<style>
|
||||
.histogram-legend {
|
||||
display:inline-block;
|
||||
padding-right:5px
|
||||
}
|
||||
.histogram-legend-dot {
|
||||
display:inline-block;
|
||||
height:10px;
|
||||
width:10px;
|
||||
border-radius:5px;
|
||||
}
|
||||
.histogram-legend-item {
|
||||
display:inline-block;
|
||||
}
|
||||
.histogram-chart {
|
||||
position:relative;
|
||||
}
|
||||
</style>
|
||||
<span ng-show="panel.spyable" class='spy panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
<div>
|
||||
<span ng-show='panel.zoomlinks && data'>
|
||||
<!--<a class='small' ng-click='zoom(0.5)'><i class='icon-zoom-in'></i> Zoom In</a>-->
|
||||
<a class='small' ng-click='zoom(2)'><i class='icon-zoom-out'></i> Zoom Out</a> |
|
||||
</span>
|
||||
<span ng-show="panel.legend" ng-repeat='series in data' class="histogram-legend">
|
||||
<i class='icon-circle' ng-style="{color: series.info.color}"></i>
|
||||
<span class='small histogram-legend-item'>{{series.info.alias}} ({{series.hits}})</span>
|
||||
</span>
|
||||
<span ng-show="panel.legend" class="small"><span ng-show="panel.value_field && panel.mode != 'count'">{{panel.value_field}}</span> {{panel.mode}} per <strong>{{panel.interval}}</strong> | (<strong>{{hits}}</strong> hits)</span>
|
||||
</div>
|
||||
<center><img ng-show='panel.loading && _.isUndefined(data)' src="img/load_big.gif"></center>
|
||||
<div histogram-chart class="pointer histogram-chart" params="{{panel}}"></div>
|
||||
</div>
|
||||
Executable
+475
@@ -0,0 +1,475 @@
|
||||
/*
|
||||
|
||||
## Histogram
|
||||
|
||||
### Parameters
|
||||
* auto_int :: Auto calculate data point interval?
|
||||
* resolution :: If auto_int is enables, shoot for this many data points, rounding to
|
||||
sane intervals
|
||||
* interval :: Datapoint interval in elasticsearch date math format (eg 1d, 1w, 1y, 5y)
|
||||
* fill :: Only applies to line charts. Level of area shading from 0-10
|
||||
* linewidth :: Only applies to line charts. How thick the line should be in pixels
|
||||
While the editor only exposes 0-10, this can be any numeric value.
|
||||
Set to 0 and you'll get something like a scatter plot
|
||||
* timezone :: This isn't totally functional yet. Currently only supports browser and utc.
|
||||
browser will adjust the x-axis labels to match the timezone of the user's
|
||||
browser
|
||||
* spyable :: Dislay the 'eye' icon that show the last elasticsearch query
|
||||
* zoomlinks :: Show the zoom links?
|
||||
* bars :: Show bars in the chart
|
||||
* stack :: Stack multiple queries. This generally a crappy way to represent things.
|
||||
You probably should just use a line chart without stacking
|
||||
* points :: Should circles at the data points on the chart
|
||||
* lines :: Line chart? Sweet.
|
||||
* legend :: Show the legend?
|
||||
* x-axis :: Show x-axis labels and grid lines
|
||||
* y-axis :: Show y-axis labels and grid lines
|
||||
* interactive :: Allow drag to select time range
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'kbn',
|
||||
'moment',
|
||||
'./timeSeries',
|
||||
|
||||
'jquery.flot',
|
||||
'jquery.flot.pie',
|
||||
'jquery.flot.selection',
|
||||
'jquery.flot.time',
|
||||
'jquery.flot.stack'
|
||||
],
|
||||
function (angular, app, $, _, kbn, moment, timeSeries) {
|
||||
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.histogram', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('histogram', function($scope, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{
|
||||
title:'Queries',
|
||||
src:'app/partials/querySelect.html'
|
||||
}
|
||||
],
|
||||
status : "Stable",
|
||||
description : "A bucketed time series chart of the current query or queries. Uses the "+
|
||||
"Elasticsearch date_histogram facet. If using time stamped indices this panel will query"+
|
||||
" them sequentially to attempt to apply the lighest possible load to your Elasticsearch cluster"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
mode : 'count',
|
||||
time_field : '@timestamp',
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
value_field : null,
|
||||
auto_int : true,
|
||||
resolution : 100,
|
||||
interval : '5m',
|
||||
fill : 0,
|
||||
linewidth : 3,
|
||||
timezone : 'browser', // browser, utc or a standard timezone
|
||||
spyable : true,
|
||||
zoomlinks : true,
|
||||
bars : true,
|
||||
stack : true,
|
||||
points : false,
|
||||
lines : false,
|
||||
legend : true,
|
||||
'x-axis' : true,
|
||||
'y-axis' : true,
|
||||
percentage : false,
|
||||
interactive : true,
|
||||
tooltip : {
|
||||
value_type: 'cumulative',
|
||||
query_as_alias: false
|
||||
}
|
||||
};
|
||||
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.$on('refresh',function(){
|
||||
$scope.get_data();
|
||||
});
|
||||
|
||||
$scope.get_data();
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* The time range effecting the panel
|
||||
* @return {[type]} [description]
|
||||
*/
|
||||
$scope.get_time_range = function () {
|
||||
var range = $scope.range = filterSrv.timeRange('min');
|
||||
return range;
|
||||
};
|
||||
|
||||
$scope.get_interval = function () {
|
||||
var interval = $scope.panel.interval,
|
||||
range;
|
||||
if ($scope.panel.auto_int) {
|
||||
range = $scope.get_time_range();
|
||||
if (range) {
|
||||
interval = kbn.secondsToHms(
|
||||
kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000
|
||||
);
|
||||
}
|
||||
}
|
||||
$scope.panel.interval = interval || '10m';
|
||||
return $scope.panel.interval;
|
||||
};
|
||||
|
||||
/**
|
||||
* Fetch the data for a chunk of a queries results. Multiple segments occur when several indicies
|
||||
* need to be consulted (like timestamped logstash indicies)
|
||||
*
|
||||
* The results of this function are stored on the scope's data property. This property will be an
|
||||
* array of objects with the properties info, time_series, and hits. These objects are used in the
|
||||
* render_panel function to create the historgram.
|
||||
*
|
||||
* @param {number} segment The segment count, (0 based)
|
||||
* @param {number} query_id The id of the query, generated on the first run and passed back when
|
||||
* this call is made recursively for more segments
|
||||
*/
|
||||
$scope.get_data = function(segment, query_id) {
|
||||
if (_.isUndefined(segment)) {
|
||||
segment = 0;
|
||||
}
|
||||
delete $scope.panel.error;
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
var _range = $scope.get_time_range();
|
||||
var _interval = $scope.get_interval(_range);
|
||||
|
||||
if ($scope.panel.auto_int) {
|
||||
$scope.panel.interval = kbn.secondsToHms(
|
||||
kbn.calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000);
|
||||
}
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices[segment]);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// Build the query
|
||||
_.each($scope.panel.queries.ids, function(id) {
|
||||
var query = $scope.ejs.FilteredQuery(
|
||||
querySrv.getEjsObj(id),
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
);
|
||||
|
||||
var facet = $scope.ejs.DateHistogramFacet(id);
|
||||
|
||||
if($scope.panel.mode === 'count') {
|
||||
facet = facet.field($scope.panel.time_field);
|
||||
} else {
|
||||
if(_.isNull($scope.panel.value_field)) {
|
||||
$scope.panel.error = "In " + $scope.panel.mode + " mode a field must be specified";
|
||||
return;
|
||||
}
|
||||
facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field);
|
||||
}
|
||||
facet = facet.interval(_interval).facetFilter($scope.ejs.QueryFilter(query));
|
||||
request = request.facet(facet).size(0);
|
||||
});
|
||||
|
||||
// Populate the inspector panel
|
||||
$scope.populate_modal(request);
|
||||
|
||||
// Then run it
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
if(segment === 0) {
|
||||
$scope.hits = 0;
|
||||
$scope.data = [];
|
||||
query_id = $scope.query_id = new Date().getTime();
|
||||
}
|
||||
|
||||
// Check for error and abort if found
|
||||
if(!(_.isUndefined(results.error))) {
|
||||
$scope.panel.error = $scope.parse_error(results.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert facet ids to numbers
|
||||
var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k, 10);});
|
||||
|
||||
// Make sure we're still on the same query/queries
|
||||
if($scope.query_id === query_id && _.difference(facetIds, $scope.panel.queries.ids).length === 0) {
|
||||
|
||||
var i = 0,
|
||||
time_series,
|
||||
hits;
|
||||
|
||||
_.each($scope.panel.queries.ids, function(id) {
|
||||
var query_results = results.facets[id];
|
||||
// we need to initialize the data variable on the first run,
|
||||
// and when we are working on the first segment of the data.
|
||||
if(_.isUndefined($scope.data[i]) || segment === 0) {
|
||||
time_series = new timeSeries.ZeroFilled({
|
||||
interval: _interval,
|
||||
start_date: _range && _range.from,
|
||||
end_date: _range && _range.to,
|
||||
fill_style: 'minimal'
|
||||
});
|
||||
hits = 0;
|
||||
} else {
|
||||
time_series = $scope.data[i].time_series;
|
||||
hits = $scope.data[i].hits;
|
||||
}
|
||||
|
||||
// push each entry into the time series, while incrementing counters
|
||||
_.each(query_results.entries, function(entry) {
|
||||
time_series.addValue(entry.time, entry[$scope.panel.mode]);
|
||||
hits += entry.count; // The series level hits counter
|
||||
$scope.hits += entry.count; // Entire dataset level hits counter
|
||||
});
|
||||
$scope.data[i] = {
|
||||
info: querySrv.list[id],
|
||||
time_series: time_series,
|
||||
hits: hits
|
||||
};
|
||||
|
||||
i++;
|
||||
});
|
||||
|
||||
// Tell the histogram directive to render.
|
||||
$scope.$emit('render');
|
||||
|
||||
// If we still have segments left, get them
|
||||
if(segment < dashboard.indices.length-1) {
|
||||
$scope.get_data(segment+1,query_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// function $scope.zoom
|
||||
// factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
|
||||
$scope.zoom = function(factor) {
|
||||
var _range = filterSrv.timeRange('min');
|
||||
var _timespan = (_range.to.valueOf() - _range.from.valueOf());
|
||||
var _center = _range.to.valueOf() - _timespan/2;
|
||||
|
||||
var _to = (_center + (_timespan*factor)/2);
|
||||
var _from = (_center - (_timespan*factor)/2);
|
||||
|
||||
// If we're not already looking into the future, don't.
|
||||
if(_to > Date.now() && _range.to < Date.now()) {
|
||||
var _offset = _to - Date.now();
|
||||
_from = _from - _offset;
|
||||
_to = Date.now();
|
||||
}
|
||||
|
||||
if(factor > 1) {
|
||||
filterSrv.removeByType('time');
|
||||
}
|
||||
filterSrv.set({
|
||||
type:'time',
|
||||
from:moment.utc(_from),
|
||||
to:moment.utc(_to),
|
||||
field:$scope.panel.time_field
|
||||
});
|
||||
|
||||
dashboard.refresh();
|
||||
|
||||
};
|
||||
|
||||
// I really don't like this function, too much dom manip. Break out into directive?
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
$scope.$emit('render');
|
||||
};
|
||||
});
|
||||
|
||||
module.directive('histogramChart', function(dashboard, filterSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '<div></div>',
|
||||
link: function(scope, elem) {
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Re-render if the window is resized
|
||||
angular.element(window).bind('resize', function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
// IE doesn't work without this
|
||||
elem.css({height:scope.panel.height || scope.row.height});
|
||||
|
||||
// Populate from the query service
|
||||
try {
|
||||
_.each(scope.data, function(series) {
|
||||
series.label = series.info.alias;
|
||||
series.color = series.info.color;
|
||||
});
|
||||
} catch(e) {return;}
|
||||
|
||||
// Set barwidth based on specified interval
|
||||
var barwidth = kbn.interval_to_seconds(scope.panel.interval)*1000;
|
||||
|
||||
var stack = scope.panel.stack ? true : null;
|
||||
|
||||
// Populate element
|
||||
try {
|
||||
var options = {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
//stackpercent: scope.panel.stack ? scope.panel.percentage : false,
|
||||
stack: scope.panel.percentage ? null : stack,
|
||||
lines: {
|
||||
show: scope.panel.lines,
|
||||
fill: scope.panel.fill/10,
|
||||
lineWidth: scope.panel.linewidth,
|
||||
steps: false
|
||||
},
|
||||
bars: {
|
||||
show: scope.panel.bars,
|
||||
fill: 1,
|
||||
barWidth: barwidth/1.8,
|
||||
zero: false,
|
||||
lineWidth: 0
|
||||
},
|
||||
points: {
|
||||
show: scope.panel.points,
|
||||
fill: 1,
|
||||
fillColor: false,
|
||||
radius: 5
|
||||
},
|
||||
shadowSize: 1
|
||||
},
|
||||
yaxis: {
|
||||
show: scope.panel['y-axis'],
|
||||
min: 0,
|
||||
max: scope.panel.percentage && scope.panel.stack ? 100 : null,
|
||||
},
|
||||
xaxis: {
|
||||
timezone: scope.panel.timezone,
|
||||
show: scope.panel['x-axis'],
|
||||
mode: "time",
|
||||
min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
|
||||
max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
|
||||
timeformat: time_format(scope.panel.interval),
|
||||
label: "Datetime",
|
||||
},
|
||||
grid: {
|
||||
backgroundColor: null,
|
||||
borderWidth: 0,
|
||||
hoverable: true,
|
||||
color: '#c8c8c8'
|
||||
}
|
||||
};
|
||||
|
||||
if(scope.panel.interactive) {
|
||||
options.selection = { mode: "x", color: '#666' };
|
||||
}
|
||||
|
||||
// when rendering stacked bars, we need to ensure each point that has data is zero-filled
|
||||
// so that the stacking happens in the proper order
|
||||
var required_times = [];
|
||||
if (scope.data.length > 1) {
|
||||
required_times = _.uniq(Array.prototype.concat.apply([], _.map(scope.data, function (query) {
|
||||
return query.time_series.getOrderedTimes();
|
||||
})).sort(), true);
|
||||
}
|
||||
|
||||
for (var i = 0; i < scope.data.length; i++) {
|
||||
scope.data[i].data = scope.data[i].time_series.getFlotPairs(required_times);
|
||||
}
|
||||
|
||||
scope.plot = $.plot(elem, scope.data, options);
|
||||
|
||||
} catch(e) {
|
||||
elem.text(e);
|
||||
}
|
||||
}
|
||||
|
||||
function time_format(interval) {
|
||||
var _int = kbn.interval_to_seconds(interval);
|
||||
if(_int >= 2628000) {
|
||||
return "%m/%y";
|
||||
}
|
||||
if(_int >= 86400) {
|
||||
return "%m/%d/%y";
|
||||
}
|
||||
if(_int >= 60) {
|
||||
return "%H:%M<br>%m/%d";
|
||||
}
|
||||
|
||||
return "%H:%M:%S";
|
||||
}
|
||||
|
||||
var $tooltip = $('<div>');
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
var group, value;
|
||||
if (item) {
|
||||
if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
|
||||
group = '<small style="font-size:0.9em;">' +
|
||||
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
|
||||
(item.series.info.alias || item.series.info.query)+
|
||||
'</small><br>';
|
||||
} else {
|
||||
group = kbn.query_color_dot(item.series.color, 15) + ' ';
|
||||
}
|
||||
if (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') {
|
||||
value = item.datapoint[1] - item.datapoint[2];
|
||||
} else {
|
||||
value = item.datapoint[1];
|
||||
}
|
||||
$tooltip
|
||||
.html(
|
||||
group + value + " @ " + moment(item.datapoint[0]).format('MM/DD HH:mm:ss')
|
||||
)
|
||||
.place_tt(pos.pageX, pos.pageY);
|
||||
} else {
|
||||
$tooltip.detach();
|
||||
}
|
||||
});
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
filterSrv.set({
|
||||
type : 'time',
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
field : scope.panel.time_field
|
||||
});
|
||||
dashboard.refresh();
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+173
@@ -0,0 +1,173 @@
|
||||
define(['underscore', 'kbn'],
|
||||
function (_, kbn) {
|
||||
'use strict';
|
||||
|
||||
var ts = {};
|
||||
|
||||
// map compatable parseInt
|
||||
function base10Int(val) {
|
||||
return parseInt(val, 10);
|
||||
}
|
||||
|
||||
// trim the ms off of a time, but return it with empty ms.
|
||||
function getDatesTime(date) {
|
||||
return Math.floor(date.getTime() / 1000)*1000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Certain graphs require 0 entries to be specified for them to render
|
||||
* properly (like the line graph). So with this we will caluclate all of
|
||||
* the expected time measurements, and fill the missing ones in with 0
|
||||
* @param {object} opts An object specifying some/all of the options
|
||||
*
|
||||
* OPTIONS:
|
||||
* @opt {string} interval The interval notion describing the expected spacing between
|
||||
* each data point.
|
||||
* @opt {date} start_date (optional) The start point for the time series, setting this and the
|
||||
* end_date will ensure that the series streches to resemble the entire
|
||||
* expected result
|
||||
* @opt {date} end_date (optional) The end point for the time series, see start_date
|
||||
* @opt {string} fill_style Either "minimal", or "all" describing the strategy used to zero-fill
|
||||
* the series.
|
||||
*/
|
||||
ts.ZeroFilled = function (opts) {
|
||||
opts = _.defaults(opts, {
|
||||
interval: '10m',
|
||||
start_date: null,
|
||||
end_date: null,
|
||||
fill_style: 'minimal'
|
||||
});
|
||||
|
||||
// the expected differenece between readings.
|
||||
this.interval_ms = base10Int(kbn.interval_to_seconds(opts.interval)) * 1000;
|
||||
|
||||
// will keep all values here, keyed by their time
|
||||
this._data = {};
|
||||
this.start_time = opts.start_date && getDatesTime(opts.start_date);
|
||||
this.end_time = opts.end_date && getDatesTime(opts.end_date);
|
||||
this.opts = opts;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a row
|
||||
* @param {int} time The time for the value, in
|
||||
* @param {any} value The value at this time
|
||||
*/
|
||||
ts.ZeroFilled.prototype.addValue = function (time, value) {
|
||||
if (time instanceof Date) {
|
||||
time = getDatesTime(time);
|
||||
} else {
|
||||
time = base10Int(time);
|
||||
}
|
||||
if (!isNaN(time)) {
|
||||
this._data[time] = (_.isUndefined(value) ? 0 : value);
|
||||
}
|
||||
this._cached_times = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get an array of the times that have been explicitly set in the series
|
||||
* @param {array} include (optional) list of timestamps to include in the response
|
||||
* @return {array} An array of integer times.
|
||||
*/
|
||||
ts.ZeroFilled.prototype.getOrderedTimes = function (include) {
|
||||
var times = _.map(_.keys(this._data), base10Int);
|
||||
if (_.isArray(include)) {
|
||||
times = times.concat(include);
|
||||
}
|
||||
return _.uniq(times.sort(), true);
|
||||
};
|
||||
|
||||
/**
|
||||
* return the rows in the format:
|
||||
* [ [time, value], [time, value], ... ]
|
||||
*
|
||||
* Heavy lifting is done by _get(Min|All)FlotPairs()
|
||||
* @param {array} required_times An array of timestamps that must be in the resulting pairs
|
||||
* @return {array}
|
||||
*/
|
||||
ts.ZeroFilled.prototype.getFlotPairs = function (required_times) {
|
||||
var times = this.getOrderedTimes(required_times),
|
||||
strategy,
|
||||
pairs;
|
||||
|
||||
if(this.opts.fill_style === 'all') {
|
||||
strategy = this._getAllFlotPairs;
|
||||
} else {
|
||||
strategy = this._getMinFlotPairs;
|
||||
}
|
||||
|
||||
pairs = _.reduce(
|
||||
times, // what
|
||||
strategy, // how
|
||||
[], // where
|
||||
this // context
|
||||
);
|
||||
|
||||
// if the start and end of the pairs are inside either the start or end time,
|
||||
// add those times to the series with null values so the graph will stretch to contain them.
|
||||
if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) {
|
||||
pairs.unshift([this.start_time, null]);
|
||||
}
|
||||
if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) {
|
||||
pairs.push([this.end_time, null]);
|
||||
}
|
||||
|
||||
return pairs;
|
||||
};
|
||||
|
||||
/**
|
||||
* ** called as a reduce stragegy in getFlotPairs() **
|
||||
* Fill zero's on either side of the current time, unless there is already a measurement there or
|
||||
* we are looking at an edge.
|
||||
* @return {array} An array of points to plot with flot
|
||||
*/
|
||||
ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) {
|
||||
var next, expected_next, prev, expected_prev;
|
||||
|
||||
// check for previous measurement
|
||||
if (i > 0) {
|
||||
prev = times[i - 1];
|
||||
expected_prev = time - this.interval_ms;
|
||||
if (prev < expected_prev) {
|
||||
result.push([expected_prev, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
// add the current time
|
||||
result.push([ time, this._data[time] || 0 ]);
|
||||
|
||||
// check for next measurement
|
||||
if (times.length > i) {
|
||||
next = times[i + 1];
|
||||
expected_next = time + this.interval_ms;
|
||||
if (next > expected_next) {
|
||||
result.push([expected_next, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* ** called as a reduce stragegy in getFlotPairs() **
|
||||
* Fill zero's to the right of each time, until the next measurement is reached or we are at the
|
||||
* last measurement
|
||||
* @return {array} An array of points to plot with flot
|
||||
*/
|
||||
ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) {
|
||||
var next, expected_next;
|
||||
|
||||
result.push([ times[i], this._data[times[i]] || 0 ]);
|
||||
next = times[i + 1];
|
||||
expected_next = times[i] + this.interval_ms;
|
||||
for(; times.length > i && next > expected_next; expected_next+= this.interval_ms) {
|
||||
result.push([expected_next, 0]);
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
return ts;
|
||||
});
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<label class="small">Style</label>
|
||||
<select class="input-small" ng-model="panel.chart" ng-options="f for f in ['bar','pie','list','total']"></select></span>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.chart == 'total' || panel.chart == 'list'">
|
||||
<label class="small">Font Size</label>
|
||||
<select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
|
||||
</div>
|
||||
<div class="span3" ng-show="panel.chart == 'bar' || panel.chart == 'pie'">
|
||||
<label class="small">Legend</label>
|
||||
<select class="input-small" ng-model="panel.counter_pos" ng-options="f for f in ['above','below','none']"></select></span>
|
||||
</div>
|
||||
<div class="span3" ng-show="panel.chart != 'total' && panel.counter_pos != 'none'">
|
||||
<label class="small" >List Format</label>
|
||||
<select class="input-small" ng-model="panel.arrangement" ng-options="f for f in ['horizontal','vertical']"></select></span>
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Donut</label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Tilt</label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Labels</label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
<div ng-controller='hits' ng-init="init()">
|
||||
<span ng-show="panel.spyable" class='spy panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
<div ng-show="panel.counter_pos == 'above' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
|
||||
<!-- vertical legend -->
|
||||
<table class="small" ng-show="panel.arrangement == 'vertical'">
|
||||
<tr ng-repeat="query in data">
|
||||
<td><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.info.alias}}</td><td>{{query.data[0][1]}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- horizontal legend -->
|
||||
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
|
||||
<span><i class="icon-circle" ng-style="{color:query.info.color}"></i> {{query.info.alias}} ({{query.data[0][1]}}) </span>
|
||||
</div><br>
|
||||
|
||||
</div>
|
||||
|
||||
<div style="clear:both"></div>
|
||||
|
||||
<div ng-show="panel.chart == 'pie' || panel.chart == 'bar'" hits-chart params="{{panel}}" style="position:relative"></div>
|
||||
|
||||
<div ng-show="panel.counter_pos == 'below' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
|
||||
<!-- vertical legend -->
|
||||
<table class="small" ng-show="panel.arrangement == 'vertical'">
|
||||
<tr ng-repeat="query in data">
|
||||
<td><i class="icon-circle" ng-style="{color:query.info.color}"></i></td> <td style="padding-right:10px;padding-left:10px;">{{query.info.alias}}</td><td>{{query.data[0][1]}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- horizontal legend -->
|
||||
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
|
||||
<span><i class="icon-circle" ng-style="{color:query.info.color}"></i></span> {{query.info.alias}} ({{query.data[0][1]}}) </span>
|
||||
</div><br>
|
||||
|
||||
</div>
|
||||
|
||||
<div ng-show="panel.chart == 'total'"><div ng-style="panel.style" style="line-height:{{panel.style['font-size']}}">{{hits}}</div></div>
|
||||
|
||||
<span ng-show="panel.chart == 'list'">
|
||||
<div ng-style="panel.style" style="display:inline-block;line-height:{{panel.style['font-size']}}" ng-repeat="query in data">
|
||||
<i class="icon-circle" style="color:{{query.info.color}}"></i> {{query.info.alias}} ({{query.hits}})
|
||||
</div>
|
||||
</span><br ng-show="panel.arrangement == 'vertical' && panel.chart == 'list'">
|
||||
|
||||
</div>
|
||||
Executable
+262
@@ -0,0 +1,262 @@
|
||||
/*
|
||||
|
||||
## Hits
|
||||
|
||||
### Parameters
|
||||
* style :: A hash of css styles
|
||||
* arrangement :: How should I arrange the query results? 'horizontal' or 'vertical'
|
||||
* chart :: Show a chart? 'none', 'bar', 'pie'
|
||||
* donut :: Only applies to 'pie' charts. Punches a hole in the chart for some reason
|
||||
* tilt :: Only 'pie' charts. Janky 3D effect. Looks terrible 90% of the time.
|
||||
* lables :: Only 'pie' charts. Labels on the pie?
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'kbn',
|
||||
|
||||
'jquery.flot',
|
||||
'jquery.flot.pie'
|
||||
], function (angular, app, _, $, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.hits', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('hits', function($scope, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{title:'Queries', src:'app/partials/querySelect.html'}
|
||||
],
|
||||
status : "Stable",
|
||||
description : "The total hits for a query or set of queries. Can be a pie chart, bar chart, "+
|
||||
"list, or absolute total of all queries combined"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
style : { "font-size": '10pt'},
|
||||
arrangement : 'horizontal',
|
||||
chart : 'bar',
|
||||
counter_pos : 'above',
|
||||
donut : false,
|
||||
tilt : false,
|
||||
labels : true,
|
||||
spyable : true
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function () {
|
||||
$scope.hits = 0;
|
||||
|
||||
$scope.$on('refresh',function(){
|
||||
$scope.get_data();
|
||||
});
|
||||
$scope.get_data();
|
||||
|
||||
};
|
||||
|
||||
$scope.get_data = function(segment,query_id) {
|
||||
delete $scope.panel.error;
|
||||
$scope.panelMeta.loading = true;
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var _segment = _.isUndefined(segment) ? 0 : segment;
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// Build the question part of the query
|
||||
_.each($scope.panel.queries.ids, function(id) {
|
||||
var _q = $scope.ejs.FilteredQuery(
|
||||
querySrv.getEjsObj(id),
|
||||
filterSrv.getBoolFilter(filterSrv.ids));
|
||||
|
||||
request = request
|
||||
.facet($scope.ejs.QueryFacet(id)
|
||||
.query(_q)
|
||||
).size(0);
|
||||
});
|
||||
|
||||
// Populate the inspector panel
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
|
||||
// Then run it
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
if(_segment === 0) {
|
||||
$scope.hits = 0;
|
||||
$scope.data = [];
|
||||
query_id = $scope.query_id = new Date().getTime();
|
||||
}
|
||||
|
||||
// Check for error and abort if found
|
||||
if(!(_.isUndefined(results.error))) {
|
||||
$scope.panel.error = $scope.parse_error(results.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Convert facet ids to numbers
|
||||
var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k, 10);});
|
||||
|
||||
// Make sure we're still on the same query/queries
|
||||
if($scope.query_id === query_id &&
|
||||
_.intersection(facetIds,$scope.panel.queries.ids).length === $scope.panel.queries.ids.length
|
||||
) {
|
||||
var i = 0;
|
||||
_.each($scope.panel.queries.ids, function(id) {
|
||||
var v = results.facets[id];
|
||||
var hits = _.isUndefined($scope.data[i]) || _segment === 0 ?
|
||||
v.count : $scope.data[i].hits+v.count;
|
||||
$scope.hits += v.count;
|
||||
|
||||
// Create series
|
||||
$scope.data[i] = {
|
||||
info: querySrv.list[id],
|
||||
id: id,
|
||||
hits: hits,
|
||||
data: [[i,hits]]
|
||||
};
|
||||
|
||||
i++;
|
||||
});
|
||||
$scope.$emit('render');
|
||||
if(_segment < dashboard.indices.length-1) {
|
||||
$scope.get_data(_segment+1,query_id);
|
||||
}
|
||||
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
$scope.$emit('render');
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
module.directive('hitsChart', function(querySrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Re-render if the window is resized
|
||||
angular.element(window).bind('resize', function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
// IE doesn't work without this
|
||||
elem.css({height:scope.panel.height||scope.row.height});
|
||||
|
||||
try {
|
||||
_.each(scope.data,function(series) {
|
||||
series.label = series.info.alias;
|
||||
series.color = series.info.color;
|
||||
});
|
||||
} catch(e) {return;}
|
||||
|
||||
// Populate element
|
||||
try {
|
||||
// Add plot to scope so we can build out own legend
|
||||
if(scope.panel.chart === 'bar') {
|
||||
scope.plot = $.plot(elem, scope.data, {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
lines: { show: false, },
|
||||
bars: { show: true, fill: 1, barWidth: 0.8, horizontal: false },
|
||||
shadowSize: 1
|
||||
},
|
||||
yaxis: { show: true, min: 0, color: "#c8c8c8" },
|
||||
xaxis: { show: false },
|
||||
grid: {
|
||||
borderWidth: 0,
|
||||
borderColor: '#eee',
|
||||
color: "#eee",
|
||||
hoverable: true,
|
||||
},
|
||||
colors: querySrv.colors
|
||||
});
|
||||
}
|
||||
if(scope.panel.chart === 'pie') {
|
||||
scope.plot = $.plot(elem, scope.data, {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
pie: {
|
||||
innerRadius: scope.panel.donut ? 0.4 : 0,
|
||||
tilt: scope.panel.tilt ? 0.45 : 1,
|
||||
radius: 1,
|
||||
show: true,
|
||||
combine: {
|
||||
color: '#999',
|
||||
label: 'The Rest'
|
||||
},
|
||||
stroke: {
|
||||
width: 0
|
||||
},
|
||||
label: {
|
||||
show: scope.panel.labels,
|
||||
radius: 2/3,
|
||||
formatter: function(label, series){
|
||||
return '<div ng-click="build_search(panel.query.field,\''+label+'\')'+
|
||||
' "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
|
||||
label+'<br/>'+Math.round(series.percent)+'%</div>';
|
||||
},
|
||||
threshold: 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
//grid: { hoverable: true, clickable: true },
|
||||
grid: { hoverable: true, clickable: true },
|
||||
colors: querySrv.colors
|
||||
});
|
||||
}
|
||||
} catch(e) {
|
||||
elem.text(e);
|
||||
}
|
||||
}
|
||||
|
||||
var $tooltip = $('<div>');
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
if (item) {
|
||||
var value = scope.panel.chart === 'bar' ?
|
||||
item.datapoint[1] : item.datapoint[1][0][1];
|
||||
$tooltip
|
||||
.html(kbn.query_color_dot(item.series.color, 20) + ' ' + value.toFixed(0))
|
||||
.place_tt(pos.pageX, pos.pageY);
|
||||
} else {
|
||||
$tooltip.remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+11
@@ -0,0 +1,11 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<form>
|
||||
<h6>Field <tip>2 letter country or state code</tip></h6><h6>Field</h6>
|
||||
<input bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.field">
|
||||
</form>
|
||||
</div>
|
||||
<div class="span1"><h6>Map</h6>
|
||||
<select ng-change="$emit('render')" class="input-small" ng-model="panel.map" ng-options="f for f in ['world','europe','usa']"></select>
|
||||
</div>
|
||||
</div>
|
||||
+8
File diff suppressed because one or more lines are too long
Executable
+1
File diff suppressed because one or more lines are too long
Executable
+1
File diff suppressed because one or more lines are too long
Executable
+1
File diff suppressed because one or more lines are too long
Executable
+66
@@ -0,0 +1,66 @@
|
||||
<div ng-controller='map' ng-init="init()">
|
||||
<style>
|
||||
.jvectormap-label {
|
||||
position: absolute;
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
border: solid 1px #CDCDCD;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
background: #292929;
|
||||
color: white;
|
||||
font-family: sans-serif, Verdana;
|
||||
font-size: smaller;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.jvectormap-zoomin, .jvectormap-zoomout {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
-webkit-border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
border-radius: 3px;
|
||||
background: #292929;
|
||||
padding: 3px;
|
||||
color: white;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
cursor: pointer;
|
||||
line-height: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.jvectormap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.jvectormap-zoomin {
|
||||
display: none;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
.jvectormap-zoomout {
|
||||
display: none;
|
||||
top: 30px;
|
||||
}
|
||||
|
||||
.map-legend {
|
||||
color : #c8c8c8;
|
||||
padding : 10px;
|
||||
font-size: 11pt;
|
||||
font-weight: 200;
|
||||
background-color: #1f1f1f;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
top: 15px;
|
||||
display: none;
|
||||
z-index: 99;
|
||||
}
|
||||
</style>
|
||||
<span ng-show="panel.spyable" class='spy panelextra pointer'>
|
||||
<i bs-modal="'app/partials/modal.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
<div class="jvectormap" map params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
|
||||
</div>
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
|
||||
## Map
|
||||
|
||||
### Parameters
|
||||
* map :: 'world', 'us' or 'europe'
|
||||
* colors :: an array of colors to use for the regions of the map. If this is a 2
|
||||
element array, jquerymap will generate shades between these colors
|
||||
* size :: How big to make the facet. Higher = more countries
|
||||
* exclude :: Exlude the array of counties
|
||||
* spyable :: Show the 'eye' icon that reveals the last ES query
|
||||
* index_limit :: This does nothing yet. Eventually will limit the query to the first
|
||||
N indices
|
||||
|
||||
*/
|
||||
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'config',
|
||||
'./lib/jquery.jvectormap.min'
|
||||
],
|
||||
function (angular, app, _, $, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.map', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('map', function($scope, $rootScope, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{title:'Queries', src:'app/partials/querySelect.html'}
|
||||
],
|
||||
status : "Stable",
|
||||
description : "Displays a map of shaded regions using a field containing a 2 letter country "+
|
||||
", or US state, code. Regions with more hit are shaded darker. Node that this does use the"+
|
||||
" Elasticsearch terms facet, so it is important that you set it to the correct field."
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
map : "world",
|
||||
colors : ['#A0E2E2', '#265656'],
|
||||
size : 100,
|
||||
exclude : [],
|
||||
spyable : true,
|
||||
index_limit : 0
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.$on('refresh',function(){$scope.get_data();});
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
$scope.panelMeta.loading = true;
|
||||
|
||||
|
||||
var request;
|
||||
request = $scope.ejs.Request().indices(dashboard.indices);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// This could probably be changed to a BoolFilter
|
||||
var boolQuery = $scope.ejs.BoolQuery();
|
||||
_.each($scope.panel.queries.ids,function(id) {
|
||||
boolQuery = boolQuery.should(querySrv.getEjsObj(id));
|
||||
});
|
||||
|
||||
// Then the insert into facet and make the request
|
||||
request = request
|
||||
.facet($scope.ejs.TermsFacet('map')
|
||||
.field($scope.panel.field)
|
||||
.size($scope.panel.size)
|
||||
.exclude($scope.panel.exclude)
|
||||
.facetFilter($scope.ejs.QueryFilter(
|
||||
$scope.ejs.FilteredQuery(
|
||||
boolQuery,
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
)))).size(0);
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.hits = results.hits.total;
|
||||
$scope.data = {};
|
||||
_.each(results.facets.map.terms, function(v) {
|
||||
$scope.data[v.term.toUpperCase()] = v.count;
|
||||
});
|
||||
$scope.$emit('render');
|
||||
});
|
||||
};
|
||||
|
||||
// I really don't like this function, too much dom manip. Break out into directive?
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.modal = {
|
||||
title: "Inspector",
|
||||
body : "<h5>Last Elasticsearch Query</h5><pre>"+
|
||||
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
|
||||
angular.toJson(JSON.parse(request.toString()),true)+
|
||||
"'</pre>",
|
||||
};
|
||||
};
|
||||
|
||||
$scope.build_search = function(field,value) {
|
||||
filterSrv.set({type:'querystring',mandate:'must',query:field+":"+value});
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
|
||||
module.directive('map', function() {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
|
||||
elem.html('<center><img src="img/load_big.gif"></center>');
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Or if the window is resized
|
||||
angular.element(window).bind('resize', function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function render_panel() {
|
||||
elem.text('');
|
||||
$('.jvectormap-zoomin,.jvectormap-zoomout,.jvectormap-label').remove();
|
||||
require(['./lib/map.'+scope.panel.map], function () {
|
||||
elem.vectorMap({
|
||||
map: scope.panel.map,
|
||||
regionStyle: {initial: {fill: '#8c8c8c'}},
|
||||
zoomOnScroll: false,
|
||||
backgroundColor: null,
|
||||
series: {
|
||||
regions: [{
|
||||
values: scope.data,
|
||||
scale: scope.panel.colors,
|
||||
normalizeFunction: 'polynomial'
|
||||
}]
|
||||
},
|
||||
onRegionLabelShow: function(event, label, code){
|
||||
elem.children('.map-legend').show();
|
||||
var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
|
||||
elem.children('.map-legend').text(label.text() + ": " + count);
|
||||
},
|
||||
onRegionOut: function() {
|
||||
$('.map-legend').hide();
|
||||
},
|
||||
onRegionClick: function(event, code) {
|
||||
var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
|
||||
if (count !== 0) {
|
||||
scope.build_search(scope.panel.field,code);
|
||||
}
|
||||
}
|
||||
});
|
||||
elem.prepend('<span class="map-legend"></span>');
|
||||
$('.map-legend').hide();
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+49
@@ -0,0 +1,49 @@
|
||||
<div class="row-fluid" ng-switch="panel.mode">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Mode</label>
|
||||
<select class="input-small" ng-change="set_mode(panel.mode);set_refresh(true)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="terms">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Field</label>
|
||||
<input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.query.field" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Length</label>
|
||||
<input class="input-small" type="number" ng-model="panel.size" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
<div class="span6">
|
||||
<label class="small">Exclude Terms(s) (comma seperated)</label>
|
||||
<input array-join type="text" ng-model='panel.exclude'></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="goal">
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<form style="margin-bottom: 0px">
|
||||
<label class="small">Goal</label>
|
||||
<input type="number" style="width:90%" ng-model="panel.query.goal" ng-change="set_refresh(true)">
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span1">
|
||||
<label class="small"> Donut </label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small"> Tilt </label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small"> Labels </label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Legend</label>
|
||||
<select class="input-small" ng-model="panel.legend" ng-options="f for f in ['above','below','none']"></select></span>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
<div ng-controller='pie' ng-init="init()">
|
||||
<span ng-show='panel.spyable' style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
|
||||
<i bs-modal="'app/partials/modal.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
|
||||
<div ng-show="panel.legend == 'above'" ng-repeat="query in legend" style="float:left;padding-left: 10px;">
|
||||
<span ng-show='panel.chart != "none"'><i class="icon-circle" ng-style="{color:query.color}"></i></span><span class="small"> {{query.label}} ({{query.data[0][1]}}) </span></span>
|
||||
</div><br>
|
||||
<div style="clear:both"></div>
|
||||
|
||||
<div pie class="pointer" params="{{panel}}" style="position:relative"></div>
|
||||
|
||||
<div ng-show="panel.legend == 'below'" ng-repeat="query in legend" style="float:left;padding-left: 10px;">
|
||||
<span ng-show='panel.chart != "none"'><i class="icon-circle" ng-style="{color:query.color}"></i></span><span class="small"> {{query.label}} ({{query.data[0][1]}}) </span></span>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+300
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
|
||||
## Pie
|
||||
|
||||
### Parameters
|
||||
* query :: An object with 2 possible parameters depends on the mode:
|
||||
** field: Fields to run a terms facet on. Only does anything in terms mode
|
||||
** goal: How many to shoot for, only does anything in goal mode
|
||||
* exclude :: In terms mode, ignore these terms
|
||||
* donut :: Drill a big hole in the pie
|
||||
* tilt :: A janky 3D representation of the pie. Looks terrible 90% of the time.
|
||||
* legend :: Show the legend?
|
||||
* labels :: Label the slices of the pie?
|
||||
* mode :: 'terms' or 'goal'
|
||||
* default_field :: LOL wat? A dumb fail over field if for some reason the query object
|
||||
doesn't have a field
|
||||
* spyable :: Show the 'eye' icon that displays the last ES query for this panel
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'kbn',
|
||||
'config'
|
||||
], function (angular, app, _, $, kbn, config) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.pie', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('pie', function($scope, $rootScope, querySrv, dashboard, filterSrv) {
|
||||
|
||||
$scope.panelMeta = {
|
||||
status : "Deprecated",
|
||||
description : "Uses an Elasticsearch terms facet to create a pie chart. You should really only"+
|
||||
" point this at not_analyzed fields for that reason. This panel is going away soon, it has"+
|
||||
" <strong>been replaced by the terms panel</strong>. Please use that one instead."
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
editorTabs : [
|
||||
{title:'Queries', src:'app/partials/querySelect.html'}
|
||||
],
|
||||
query : { field:"_type", goal: 100},
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
size : 10,
|
||||
exclude : [],
|
||||
donut : false,
|
||||
tilt : false,
|
||||
legend : "above",
|
||||
labels : true,
|
||||
mode : "terms",
|
||||
default_field : 'DEFAULT',
|
||||
spyable : true,
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.$on('refresh',function(){$scope.get_data();});
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.set_mode = function(mode) {
|
||||
switch(mode)
|
||||
{
|
||||
case 'terms':
|
||||
$scope.panel.query = {field:"_all"};
|
||||
break;
|
||||
case 'goal':
|
||||
$scope.panel.query = {goal:100};
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
$scope.$emit('render');
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// This could probably be changed to a BoolFilter
|
||||
var boolQuery = $scope.ejs.BoolQuery();
|
||||
_.each($scope.panel.queries.ids,function(id) {
|
||||
boolQuery = boolQuery.should(querySrv.getEjsObj(id));
|
||||
});
|
||||
|
||||
|
||||
var results;
|
||||
|
||||
// Terms mode
|
||||
if ($scope.panel.mode === "terms") {
|
||||
request = request
|
||||
.facet($scope.ejs.TermsFacet('pie')
|
||||
.field($scope.panel.query.field || $scope.panel.default_field)
|
||||
.size($scope.panel.size)
|
||||
.exclude($scope.panel.exclude)
|
||||
.facetFilter($scope.ejs.QueryFilter(
|
||||
$scope.ejs.FilteredQuery(
|
||||
boolQuery,
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
)))).size(0);
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.hits = results.hits.total;
|
||||
$scope.data = [];
|
||||
var k = 0;
|
||||
_.each(results.facets.pie.terms, function(v) {
|
||||
var slice = { label : v.term, data : v.count };
|
||||
$scope.data.push();
|
||||
$scope.data.push(slice);
|
||||
k = k + 1;
|
||||
});
|
||||
$scope.$emit('render');
|
||||
});
|
||||
// Goal mode
|
||||
} else {
|
||||
request = request
|
||||
.query(boolQuery)
|
||||
.filter(filterSrv.getBoolFilter(filterSrv.ids))
|
||||
.size(0);
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
results = request.doSearch();
|
||||
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
var complete = results.hits.total;
|
||||
var remaining = $scope.panel.query.goal - complete;
|
||||
$scope.data = [
|
||||
{ label : 'Complete', data : complete, color: '#BF6730' },
|
||||
{ data : remaining, color: '#e2d0c4' }
|
||||
];
|
||||
$scope.$emit('render');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// I really don't like this function, too much dom manip. Break out into directive?
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.modal = {
|
||||
title: "Inspector",
|
||||
body : "<h5>Last Elasticsearch Query</h5><pre>"+
|
||||
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
|
||||
angular.toJson(JSON.parse(request.toString()),true)+
|
||||
"'</pre>",
|
||||
};
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('pie', function(querySrv, filterSrv, dashboard) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
|
||||
elem.html('<center><img src="img/load_big.gif"></center>');
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Or if the window is resized
|
||||
angular.element(window).bind('resize', function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
// IE doesn't work without this
|
||||
elem.css({height:scope.panel.height||scope.row.height});
|
||||
|
||||
var label;
|
||||
|
||||
if(scope.panel.mode === 'goal') {
|
||||
label = {
|
||||
show: scope.panel.labels,
|
||||
radius: 0,
|
||||
formatter: function(label, series){
|
||||
var font = parseInt(scope.row.height.replace('px',''),10)/8 + String('px');
|
||||
if(!(_.isUndefined(label))) {
|
||||
return '<div style="font-size:'+font+';font-weight:bold;text-align:center;padding:2px;color:#fff;">'+
|
||||
Math.round(series.percent)+'%</div>';
|
||||
} else {
|
||||
return '';
|
||||
}
|
||||
},
|
||||
};
|
||||
} else {
|
||||
label = {
|
||||
show: scope.panel.labels,
|
||||
radius: 2/3,
|
||||
formatter: function(label, series){
|
||||
return '<div "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
|
||||
label+'<br/>'+Math.round(series.percent)+'%</div>';
|
||||
},
|
||||
threshold: 0.1
|
||||
};
|
||||
}
|
||||
|
||||
var pie = {
|
||||
series: {
|
||||
pie: {
|
||||
innerRadius: scope.panel.donut ? 0.45 : 0,
|
||||
tilt: scope.panel.tilt ? 0.45 : 1,
|
||||
radius: 1,
|
||||
show: true,
|
||||
combine: {
|
||||
color: '#999',
|
||||
label: 'The Rest'
|
||||
},
|
||||
label: label,
|
||||
stroke: {
|
||||
width: 0
|
||||
}
|
||||
}
|
||||
},
|
||||
//grid: { hoverable: true, clickable: true },
|
||||
grid: {
|
||||
backgroundColor: null,
|
||||
hoverable: true,
|
||||
clickable: true
|
||||
},
|
||||
legend: { show: false },
|
||||
colors: querySrv.colors
|
||||
};
|
||||
|
||||
// Populate legend
|
||||
if(elem.is(":visible")){
|
||||
require(['vendor/jquery/jquery.flot.pie.js'], function(){
|
||||
scope.legend = $.plot(elem, scope.data, pie).getData();
|
||||
if(!scope.$$phase) {
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
elem.bind('plotclick', function (event, pos, object) {
|
||||
if (!object) {
|
||||
return;
|
||||
}
|
||||
if(scope.panel.mode === 'terms') {
|
||||
filterSrv.set({type:'terms',field:scope.panel.query.field,value:object.series.label});
|
||||
dashboard.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
var $tooltip = $('<div>');
|
||||
elem.bind('plothover', function (event, pos, item) {
|
||||
if (item) {
|
||||
$tooltip
|
||||
.html([
|
||||
kbn.query_color_dot(item.series.color, 15),
|
||||
(item.series.label || ''),
|
||||
parseFloat(item.series.percent).toFixed(1) + '%'
|
||||
].join(' '))
|
||||
.place_tt(pos.pageX, pos.pageY, {
|
||||
offset: 10
|
||||
});
|
||||
} else {
|
||||
$tooltip.remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span12">
|
||||
No options here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
<div class="panel-query-meta row-fluid" style="width:170px">
|
||||
|
||||
<style>
|
||||
.input-query-alias {
|
||||
margin-bottom: 5px !important;
|
||||
}
|
||||
.panel-query-meta .pin {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
</style>
|
||||
<a class="close" ng-click="render();dismiss();" href="">×</a>
|
||||
<i ng-click="toggle_pin(id);dismiss();" class="small pointer icon-pushpin"></i>
|
||||
<label class="strong small ">Query Alias</label>
|
||||
<form>
|
||||
<input class="input-medium input-query-alias" type="text" ng-model="querySrv.list[id].alias" placeholder='Alias...' />
|
||||
<div>
|
||||
<i ng-repeat="color in querySrv.colors" class="pointer" ng-class="{'icon-circle-blank':querySrv.list[id].color == color,'icon-circle':querySrv.list[id].color != color}" ng-style="{color:color}" ng-click="querySrv.list[id].color = color;render();"> </i>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
<div ng-controller='query' ng-init="init()" class="query-panel">
|
||||
<label class="small">{{panel.label}}</label>
|
||||
<div ng-repeat="id in (unPinnedQueries = (querySrv.ids|pinnedQuery:false))" ng-class="{'short-query': unPinnedQueries.length>1}">
|
||||
<form class="form-search" style="position:relative;margin-bottom:5px;" ng-submit="refresh()">
|
||||
<span class="begin-query">
|
||||
<i class="icon-circle pointer" data-unique="1" bs-popover="'app/panels/query/meta.html'" data-placement="right" ng-style="{color: querySrv.list[id].color}"></i>
|
||||
<i class="icon-remove-sign pointer remove-query" ng-show="querySrv.ids.length > 1" ng-click="querySrv.remove(id);refresh()"></i>
|
||||
</span>
|
||||
<input class="search-query panel-query" ng-class="{ 'input-block-level': unPinnedQueries.length==1, 'last-query': $last, 'has-remove': querySrv.ids.length > 1 }" bs-typeahead="panel.history" data-min-length=0 data-items=100 type="text" ng-model="querySrv.list[id].query" />
|
||||
<span class="end-query">
|
||||
<i class="icon-search pointer" ng-click="refresh()" ng-show="$last"></i>
|
||||
<i class="icon-plus pointer" ng-click="querySrv.set({})" ng-show="$last"></i>
|
||||
</span>
|
||||
</form>
|
||||
</div>
|
||||
<div style="display:inline-block" ng-repeat="id in querySrv.ids|pinnedQuery:true">
|
||||
<span class="pointer" ng-show="$first" ng-click="panel.pinned = !panel.pinned"><small class="pins">Pinned</small> <i ng-class="{'icon-caret-right':panel.pinned,'icon-caret-left':!panel.pinned}"></i></span>
|
||||
<span ng-show="panel.pinned" class="pinned badge">
|
||||
<i class="icon-circle pointer" ng-style="{color: querySrv.list[id].color}" data-unique="1" bs-popover="'app/panels/query/meta.html'"></i><span bs-tooltip="querySrv.list[id].query"> {{querySrv.list[id].alias || querySrv.list[id].query}}</span>
|
||||
</span>
|
||||
</div>
|
||||
<span style="display:inline-block" ng-show="unPinnedQueries.length == 0">
|
||||
<i class="icon-search pointer" ng-click="refresh()"></i>
|
||||
<i class="icon-plus pointer" ng-click="querySrv.set({})"></i>
|
||||
</span>
|
||||
</div>
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
|
||||
## query
|
||||
|
||||
### Parameters
|
||||
* label :: The label to stick over the field
|
||||
* query :: A string or an array of querys. String if multi is off, array if it is on
|
||||
This should be fixed, it should always be an array even if its only
|
||||
one element
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
|
||||
'css!./query.css'
|
||||
], function (angular, app, _) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.query', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('query', function($scope, querySrv, $rootScope) {
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "Manage all of the queries on the dashboard. You almost certainly need one of "+
|
||||
"these somewhere. This panel allows you to add, remove, label, pin and color queries"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
label : "Search",
|
||||
query : "*",
|
||||
pinned : true,
|
||||
history : [],
|
||||
remember: 10 // max: 100, angular strap can't take a variable for items param
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.querySrv = querySrv;
|
||||
|
||||
$scope.init = function() {
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
update_history(_.pluck($scope.querySrv.list,'query'));
|
||||
$rootScope.$broadcast('refresh');
|
||||
};
|
||||
|
||||
$scope.render = function() {
|
||||
$rootScope.$broadcast('render');
|
||||
};
|
||||
|
||||
$scope.toggle_pin = function(id) {
|
||||
querySrv.list[id].pin = querySrv.list[id].pin ? false : true;
|
||||
};
|
||||
|
||||
var update_history = function(query) {
|
||||
if($scope.panel.remember > 0) {
|
||||
$scope.panel.history = _.union(query.reverse(),$scope.panel.history);
|
||||
var _length = $scope.panel.history.length;
|
||||
if(_length > $scope.panel.remember) {
|
||||
$scope.panel.history = $scope.panel.history.slice(0,$scope.panel.remember);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
$scope.init();
|
||||
|
||||
});
|
||||
});
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
.short-query {
|
||||
display:inline-block;
|
||||
margin-right: 10px;
|
||||
}
|
||||
.begin-query {
|
||||
position:absolute;
|
||||
left:13px;
|
||||
top:5px;
|
||||
}
|
||||
.end-query {
|
||||
position:absolute;
|
||||
right:15px;
|
||||
top:5px;
|
||||
}
|
||||
.panel-query {
|
||||
padding-left: 35px !important;
|
||||
height: 31px !important;
|
||||
-webkit-box-sizing: border-box; /* Safari/Chrome, other WebKit */
|
||||
-moz-box-sizing: border-box; /* Firefox, other Gecko */
|
||||
box-sizing: border-box; /* Opera/IE 8+ */
|
||||
}
|
||||
.form-search:hover .has-remove {
|
||||
padding-left: 50px !important;
|
||||
}
|
||||
.remove-query {
|
||||
opacity: 0;
|
||||
}
|
||||
.last-query {
|
||||
padding-right: 45px !important;
|
||||
}
|
||||
.form-search:hover .remove-query {
|
||||
opacity: 1;
|
||||
}
|
||||
.query-panel .pins {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.query-panel .pinned {
|
||||
margin-right: 5px;
|
||||
}
|
||||
Executable
+48
@@ -0,0 +1,48 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<form class="input-append">
|
||||
<h6>Add Column</h6>
|
||||
<input bs-typeahead="fields.list" type="text" class="input-small" ng-model='newfield'>
|
||||
<button class="btn" ng-click="toggle_field(newfield);newfield=''"><i class="icon-plus"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="span8">
|
||||
<h6>Columns <small>Click to remove</small></h6>
|
||||
<span style="margin-left:3px" ng-click="toggle_field(field)" ng-repeat="field in $parent.panel.fields" class="label pointer remove">{{field}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<form class="input-append">
|
||||
<h6>Add field</h6>
|
||||
<input bs-typeahead="fields.list" type="text" class="input-small" ng-model='newhighlight' ng-change="set_refresh(true)">
|
||||
<button class="btn" ng-click="toggle_highlight(newhighlight);newhighlight=''"><i class="icon-plus"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="span8">
|
||||
<h6>Highlighted fields <small>Click to remove</small></h6>
|
||||
<span style="margin-left:3px" ng-click="toggle_highlight(field);set_refresh(true)" ng-repeat="field in $parent.panel.highlight" class="label remove pointer">{{field}} </span>
|
||||
</div>
|
||||
</div>
|
||||
<h5>Options</h5>
|
||||
<div class="row-fluid">
|
||||
<div class="span1">
|
||||
<h6>Header</h6><input type="checkbox" ng-model="panel.header" ng-checked="panel.header">
|
||||
</div>
|
||||
<div class="span1">
|
||||
<h6>Sorting</h6><input type="checkbox" ng-model="panel.sortable" ng-checked="panel.sortable">
|
||||
</div>
|
||||
<div class="span3" style="white-space:nowrap" ng-show='panel.sortable'>
|
||||
<h6>Sort</h6>
|
||||
<input ng-show="all_fields.length<=0 || !all_fields"style="width:85%" ng-model="panel.sort[0]" type="text"></input>
|
||||
<select ng-show="all_fields.length>0"style="width:85%" ng-model="panel.sort[0]" ng-options="f for f in all_fields"></select>
|
||||
<i ng-click="set_sort(panel.sort[0])" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
|
||||
</div>
|
||||
<div class="span2"><h6>Font Size</h6>
|
||||
<select class="input-small" ng-model="panel.style['font-size']" ng-options="f for f in ['7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
|
||||
</div>
|
||||
<div class="span2">
|
||||
<h6>Trim Factor <tip>Trim fields to this long divided by # of rows</tip></h6>
|
||||
<input type="number" class="input-small" ng-model="panel.trimFactor">
|
||||
</div>
|
||||
</div>
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
<a class="close" ng-click="dismiss()" href="">×</a>
|
||||
<h4>
|
||||
Micro Analysis of {{micropanel.field}}
|
||||
<i class="pointer icon-search" ng-click="fieldExists(micropanel.field,'must');dismiss();"></i>
|
||||
<i class="pointer icon-ban-circle" ng-click="fieldExists(micropanel.field,'mustNot');dismiss();"></i>
|
||||
<br><small>{{micropanel.count}} events in the table set</small>
|
||||
</h4>
|
||||
<table style="width:100%" class='table table-striped table-condensed'>
|
||||
<thead>
|
||||
<th>{{micropanel.field}}</th>
|
||||
<th>Action</th>
|
||||
<th style="text-align:right">Count</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat='field in micropanel.values'>
|
||||
<td>{{{true: "__blank__", false:field[0] }[field[0] == '' || field[0] == undefined]|tableTruncate:panel.trimFactor:3}}</td>
|
||||
<td style="width:40px">
|
||||
<i class="pointer icon-search" ng-click="build_search(micropanel.field,field[0]);dismiss();"></i>
|
||||
<i class="pointer icon-ban-circle" ng-click="build_search(micropanel.field,field[0],true);dismiss();"></i>
|
||||
</td>
|
||||
<td class="progress" style="width:100px;position:relative">
|
||||
<style scoped>
|
||||
.progress {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
<div bs-tooltip="percent(field[1],data.length)" class="bar" ng-class="micropanelColor($index)" ng-style="{width: percent(field[1],data.length)}"></div>
|
||||
<span style="position:absolute;right:20px;">{{field[1]}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="progress">
|
||||
<div ng-repeat='field in micropanel.values' bs-tooltip="field[0]+' ('+percent(field[1],data.length)+')'" class="bar {{micropanelColor($index)}}" ng-style="{width: percent(field[1],data.length)};"></div>
|
||||
</div>
|
||||
<span ng-repeat='(field,count) in micropanel.related'><a ng-click="toggle_field(field)">{{field}}</a> ({{Math.round((count / micropanel.count) * 100)}}%), </span>
|
||||
Executable
+101
@@ -0,0 +1,101 @@
|
||||
<div ng-controller='table' ng-init='init()'>
|
||||
<style>
|
||||
.table-doc-table {
|
||||
margin-left: 0px !important;
|
||||
overflow-y: auto;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
</style>
|
||||
|
||||
<span ng-show="panel.spyable" style="position:absolute;right:0px;top:0px" class='panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div ng-class="{'span3':panel.field_list}" ng-show="panel.field_list">
|
||||
<div class="sidebar-nav">
|
||||
<h5>Fields <i class=" icon-chevron-sign-left pointer " ng-click="panel.field_list = !panel.field_list" bs-tooltip="'Hide field list'" ng-show="panel.field_list"></i></h5>
|
||||
<ul class="unstyled" style="{{panel.overflow}}:{{panel.height || row.height}};overflow-y:auto;overflow-x:hidden;">
|
||||
<li ng-style="panel.style" ng-repeat="field in fields.list" >
|
||||
<i class="pointer" ng-class="{'icon-check': _.contains(panel.fields,field),'icon-check-empty': !_.contains(panel.fields,field)}" ng-click="toggle_field(field)"></i>
|
||||
<a class="pointer" data-unique="1" bs-popover="'app/panels/table/micropanel.html'" data-placement="right" ng-click="toggle_micropanel(field)" ng-class="{label: _.contains(panel.fields,field)}">{{field}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div style="{{panel.overflow}}:{{panel.height || row.height}};" ng-class="{'span9':panel.field_list,'span12':!panel.field_list}" class="table-doc-table">
|
||||
<i class="pull-left icon-chevron-sign-right pointer" ng-click="panel.field_list = !panel.field_list" bs-tooltip="'Show field list'" ng-show="!panel.field_list"></i>
|
||||
<div class="row-fluid" ng-show="panel.paging">
|
||||
<div class="span1 offset1" style="text-align:right">
|
||||
<i ng-click="panel.offset = 0" ng-show="panel.offset > 0" class='icon-circle-arrow-left pointer'></i>
|
||||
<i ng-click="panel.offset = (panel.offset - panel.size)" ng-show="panel.offset > 0" class='icon-arrow-left pointer'></i>
|
||||
</div>
|
||||
<div class="span8" style="text-align:center">
|
||||
<strong>{{panel.offset}}</strong> to <strong>{{panel.offset + data.slice(panel.offset,panel.offset+panel.size).length}}</strong>
|
||||
<small> of {{data.length}} available for paging</small>
|
||||
</div>
|
||||
<div class="span1" style="text-align:left">
|
||||
<i ng-click="panel.offset = (panel.offset + panel.size)" ng-show="data.length > panel.offset+panel.size" class='icon-arrow-right pointer'></i>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table-hover table table-condensed" ng-style="panel.style">
|
||||
<thead ng-show="panel.header">
|
||||
<th ng-show="panel.fields.length<1">_source (select columns from the list to the left)</th>
|
||||
<th style="white-space:nowrap" ng-repeat="field in panel.fields">
|
||||
<i ng-show="!$first" class="pointer link icon-caret-left" ng-click="_.move(panel.fields,$index,$index-1)"></i>
|
||||
|
||||
<span class="pointer" ng-click="set_sort(field)" ng-show='panel.sortable'>
|
||||
{{field}}
|
||||
<i ng-show='field == panel.sort[0]' class="pointer link" ng-class="{'icon-chevron-up': panel.sort[1] == 'asc','icon-chevron-down': panel.sort[1] == 'desc'}"></i>
|
||||
</span>
|
||||
<span ng-show='!panel.sortable'>{{field}}</span>
|
||||
<i ng-show="!$last" class="pointer link icon-caret-right" ng-click="_.move(panel.fields,$index,$index+1)"></i>
|
||||
</th>
|
||||
|
||||
</thead>
|
||||
<tbody ng-repeat="event in data | slice:panel.offset:panel.offset+panel.size" ng-class-odd="'odd'">
|
||||
<tr ng-click="toggle_details(event)" class="pointer">
|
||||
<!--<td ng-repeat="field in panel.fields" ng-bind-html-unsafe="(event.highlight[field]||event._source[field]) | tableFieldFormat:field:event:this |tableHighlight | tableTruncate:panel.trimFactor:panel.fields.length"></td>-->
|
||||
<td ng-show="panel.fields.length<1">{{event._source|stringify|tableTruncate:panel.trimFactor:1}}</td>
|
||||
<td ng-show="panel.fields.length>0" ng-repeat="field in panel.fields" ng-bind-html-unsafe="(event.highlight[field]||event._source[field]) |tableHighlight | tableTruncate:panel.trimFactor:panel.fields.length"></td>
|
||||
</tr>
|
||||
<tr ng-show="event.kibana.details">
|
||||
<td colspan=1000>
|
||||
<table class='table table-bordered table-condensed'>
|
||||
<thead>
|
||||
<th>Field</th>
|
||||
<th>Action</th>
|
||||
<th>Value</th>
|
||||
</thead>
|
||||
<tr ng-repeat="(key,value) in event.kibana.details._source" ng-class-odd="'odd'">
|
||||
<td>{{key}}</td>
|
||||
<td style="white-space:nowrap">
|
||||
<i class='icon-search pointer' ng-click="build_search(key,value)" bs-tooltip="'Add filter to match this value'"></i>
|
||||
<i class='icon-ban-circle pointer' ng-click="build_search(key,value,true)" bs-tooltip="'Add filter to NOT match this value'"></i>
|
||||
<i class="pointer icon-th" ng-click="toggle_field(key)" bs-tooltip="'Toggle table column'"></i>
|
||||
</td>
|
||||
<!-- At some point we need to create a more efficient way of applying the filter pipeline -->
|
||||
<td style="white-space:pre-wrap" ng-bind-html-unsafe="value|noXml|urlLink|stringify"></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="row-fluid" ng-show="panel.paging">
|
||||
<div class="span1 offset3" style="text-align:right">
|
||||
<i ng-click="panel.offset = 0" ng-show="panel.offset > 0" class='icon-circle-arrow-left pointer'></i>
|
||||
<i ng-click="panel.offset = (panel.offset - panel.size)" ng-show="panel.offset > 0" class='icon-arrow-left pointer'></i>
|
||||
</div>
|
||||
<div class="span4" style="text-align:center">
|
||||
<strong>{{panel.offset}}</strong> to <strong>{{panel.offset + data.slice(panel.offset,panel.offset+panel.size).length}}</strong>
|
||||
<small> of {{data.length}} available for paging</small>
|
||||
</div>
|
||||
<div class="span1" style="text-align:left">
|
||||
<i ng-click="panel.offset = (panel.offset + panel.size)" ng-show="data.length > panel.offset+panel.size" class='icon-arrow-right pointer'></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+328
@@ -0,0 +1,328 @@
|
||||
/*
|
||||
|
||||
## Table
|
||||
|
||||
### Parameters
|
||||
* size :: Number of events per page to show
|
||||
* pages :: Number of pages to show. size * pages = number of cached events.
|
||||
Bigger = more memory usage byh the browser
|
||||
* offset :: Position from which to start in the array of hits
|
||||
* sort :: An array with 2 elements. sort[0]: field, sort[1]: direction ('asc' or 'desc')
|
||||
* style :: hash of css properties
|
||||
* fields :: columns to show in table
|
||||
* overflow :: 'height' or 'min-height' controls wether the row will expand (min-height) to
|
||||
to fit the table, or if the table will scroll to fit the row (height)
|
||||
* trimFactor :: If line is > this many characters, divided by the number of columns, trim it.
|
||||
* sortable :: Allow sorting?
|
||||
* spyable :: Show the 'eye' icon that reveals the last ES query for this panel
|
||||
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'kbn',
|
||||
'moment',
|
||||
|
||||
// 'text!./pagination.html',
|
||||
// 'text!partials/querySelect.html'
|
||||
],
|
||||
function (angular, app, _, kbn, moment) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.table', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('table', function($rootScope, $scope, fields, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{
|
||||
title:'Paging',
|
||||
src: 'app/panels/table/pagination.html'
|
||||
},
|
||||
{
|
||||
title:'Queries',
|
||||
src: 'app/partials/querySelect.html'
|
||||
}
|
||||
],
|
||||
status: "Stable",
|
||||
description: "A paginated table of records matching your query or queries. Click on a row to "+
|
||||
"expand it and review all of the fields associated with that document. <p>"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
status : "Stable",
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
size : 100, // Per page
|
||||
pages : 5, // Pages available
|
||||
offset : 0,
|
||||
sort : ['_score','desc'],
|
||||
group : "default",
|
||||
style : {'font-size': '9pt'},
|
||||
overflow: 'min-height',
|
||||
fields : [],
|
||||
highlight : [],
|
||||
sortable: true,
|
||||
header : true,
|
||||
paging : true,
|
||||
field_list: true,
|
||||
trimFactor: 300,
|
||||
normTimes : true,
|
||||
spyable : true
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function () {
|
||||
$scope.Math = Math;
|
||||
|
||||
$scope.$on('refresh',function(){$scope.get_data();});
|
||||
|
||||
$scope.fields = fields;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.percent = kbn.to_percent;
|
||||
|
||||
$scope.toggle_micropanel = function(field) {
|
||||
var docs = _.pluck($scope.data,'_source');
|
||||
$scope.micropanel = {
|
||||
field: field,
|
||||
values : kbn.top_field_values(docs,field,10),
|
||||
related : kbn.get_related_fields(docs,field),
|
||||
count: _.countBy(docs,function(doc){return _.contains(_.keys(doc),field);})['true']
|
||||
};
|
||||
};
|
||||
|
||||
$scope.micropanelColor = function(index) {
|
||||
var _c = ['bar-success','bar-warning','bar-danger','bar-info','bar-primary'];
|
||||
return index > _c.length ? '' : _c[index];
|
||||
};
|
||||
|
||||
$scope.set_sort = function(field) {
|
||||
if($scope.panel.sort[0] === field) {
|
||||
$scope.panel.sort[1] = $scope.panel.sort[1] === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
$scope.panel.sort[0] = field;
|
||||
}
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.toggle_field = function(field) {
|
||||
if (_.indexOf($scope.panel.fields,field) > -1) {
|
||||
$scope.panel.fields = _.without($scope.panel.fields,field);
|
||||
} else {
|
||||
$scope.panel.fields.push(field);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggle_highlight = function(field) {
|
||||
if (_.indexOf($scope.panel.highlight,field) > -1) {
|
||||
$scope.panel.highlight = _.without($scope.panel.highlight,field);
|
||||
} else {
|
||||
$scope.panel.highlight.push(field);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.toggle_details = function(row) {
|
||||
row.kibana = row.kibana || {};
|
||||
row.kibana.details = !row.kibana.details ? $scope.without_kibana(row) : false;
|
||||
};
|
||||
|
||||
$scope.page = function(page) {
|
||||
$scope.panel.offset = page*$scope.panel.size;
|
||||
$scope.get_data();
|
||||
};
|
||||
|
||||
$scope.build_search = function(field,value,negate) {
|
||||
var query;
|
||||
// This needs to be abstracted somewhere
|
||||
if(_.isArray(value)) {
|
||||
query = "(" + _.map(value,function(v){return angular.toJson(v);}).join(" AND ") + ")";
|
||||
} else if (_.isUndefined(value)) {
|
||||
query = '*';
|
||||
negate = !negate;
|
||||
} else {
|
||||
query = angular.toJson(value);
|
||||
}
|
||||
filterSrv.set({type:'field',field:field,query:query,mandate:(negate ? 'mustNot':'must')});
|
||||
$scope.panel.offset = 0;
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
$scope.fieldExists = function(field,mandate) {
|
||||
filterSrv.set({type:'exists',field:field,mandate:mandate});
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
$scope.get_data = function(segment,query_id) {
|
||||
$scope.panel.error = false;
|
||||
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
|
||||
var _segment = _.isUndefined(segment) ? 0 : segment;
|
||||
$scope.segment = _segment;
|
||||
|
||||
var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
|
||||
|
||||
var boolQuery = $scope.ejs.BoolQuery();
|
||||
_.each($scope.panel.queries.ids,function(id) {
|
||||
boolQuery = boolQuery.should(querySrv.getEjsObj(id));
|
||||
});
|
||||
|
||||
request = request.query(
|
||||
$scope.ejs.FilteredQuery(
|
||||
boolQuery,
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
))
|
||||
.highlight(
|
||||
$scope.ejs.Highlight($scope.panel.highlight)
|
||||
.fragmentSize(2147483647) // Max size of a 32bit unsigned int
|
||||
.preTags('@start-highlight@')
|
||||
.postTags('@end-highlight@')
|
||||
)
|
||||
.size($scope.panel.size*$scope.panel.pages)
|
||||
.sort($scope.panel.sort[0],$scope.panel.sort[1]);
|
||||
|
||||
$scope.populate_modal(request);
|
||||
|
||||
var results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
$scope.panelMeta.loading = false;
|
||||
|
||||
if(_segment === 0) {
|
||||
$scope.hits = 0;
|
||||
$scope.data = [];
|
||||
query_id = $scope.query_id = new Date().getTime();
|
||||
}
|
||||
|
||||
// Check for error and abort if found
|
||||
if(!(_.isUndefined(results.error))) {
|
||||
$scope.panel.error = $scope.parse_error(results.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that we're still on the same query, if not stop
|
||||
if($scope.query_id === query_id) {
|
||||
$scope.data= $scope.data.concat(_.map(results.hits.hits, function(hit) {
|
||||
return {
|
||||
_source : kbn.flatten_json(hit._source),
|
||||
highlight : kbn.flatten_json(hit.highlight||{}),
|
||||
_type : hit._type,
|
||||
_index : hit._index,
|
||||
_id : hit._id,
|
||||
_sort : hit.sort
|
||||
};
|
||||
}));
|
||||
|
||||
$scope.hits += results.hits.total;
|
||||
|
||||
// Sort the data
|
||||
$scope.data = _.sortBy($scope.data, function(v){
|
||||
return v._sort[0];
|
||||
});
|
||||
|
||||
// Reverse if needed
|
||||
if($scope.panel.sort[1] === 'desc') {
|
||||
$scope.data.reverse();
|
||||
}
|
||||
|
||||
// Keep only what we need for the set
|
||||
$scope.data = $scope.data.slice(0,$scope.panel.size * $scope.panel.pages);
|
||||
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
// If we're not sorting in reverse chrono order, query every index for
|
||||
// size*pages results
|
||||
// Otherwise, only get size*pages results then stop querying
|
||||
if (($scope.data.length < $scope.panel.size*$scope.panel.pages ||
|
||||
!((_.contains(filterSrv.timeField(),$scope.panel.sort[0])) && $scope.panel.sort[1] === 'desc')) &&
|
||||
_segment+1 < dashboard.indices.length) {
|
||||
$scope.get_data(_segment+1,$scope.query_id);
|
||||
}
|
||||
|
||||
});
|
||||
};
|
||||
|
||||
$scope.populate_modal = function(request) {
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
};
|
||||
|
||||
$scope.without_kibana = function (row) {
|
||||
return {
|
||||
_source : row._source,
|
||||
highlight : row.highlight
|
||||
};
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
};
|
||||
|
||||
|
||||
});
|
||||
|
||||
// This also escapes some xml sequences
|
||||
module.filter('tableHighlight', function() {
|
||||
return function(text) {
|
||||
if (!_.isUndefined(text) && !_.isNull(text) && text.toString().length > 0) {
|
||||
return text.toString().
|
||||
replace(/&/g, '&').
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>').
|
||||
replace(/\r?\n/g, '<br/>').
|
||||
replace(/@start-highlight@/g, '<code class="highlight">').
|
||||
replace(/@end-highlight@/g, '</code>');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('tableTruncate', function() {
|
||||
return function(text,length,factor) {
|
||||
if (!_.isUndefined(text) && !_.isNull(text) && text.toString().length > 0) {
|
||||
return text.length > length/factor ? text.substr(0,length/factor)+'...' : text;
|
||||
}
|
||||
return '';
|
||||
};
|
||||
});
|
||||
|
||||
// WIP
|
||||
module.filter('tableFieldFormat', function(fields){
|
||||
return function(text,field,event,scope) {
|
||||
var type;
|
||||
if(
|
||||
!_.isUndefined(fields.mapping[event._index]) &&
|
||||
!_.isUndefined(fields.mapping[event._index][event._type])
|
||||
) {
|
||||
type = fields.mapping[event._index][event._type][field]['type'];
|
||||
if(type === 'date' && scope.panel.normTimes) {
|
||||
return moment(text).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
}
|
||||
return text;
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+27
@@ -0,0 +1,27 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<h6>Show Controls</h6><input type="checkbox" ng-model="panel.paging" ng-checked="panel.paging">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<h6>Overflow</h6>
|
||||
<select class="input-small" ng-model="panel.overflow" ng-options="f.value as f.key for f in [{key:'scroll',value:'height'},{key:'expand',value:'min-height'}]"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<h6>Per Page</h6>
|
||||
<input type="number" class="input-mini" ng-model="panel.size" ng-change="get_data()">
|
||||
</div>
|
||||
<div class="span1">
|
||||
<h6> </h6>
|
||||
<center><i class='icon-remove'></i><center>
|
||||
</div>
|
||||
<div class="span2">
|
||||
<h6>Page limit</h6>
|
||||
<input type="number" class="input-mini" ng-model="panel.pages" ng-change="get_data()">
|
||||
</div>
|
||||
<div class="span2 large">
|
||||
<h6>Pageable</h6>
|
||||
<strong class="large">= {{panel.size * panel.pages}}</strong>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Field</label>
|
||||
<input type="text" class="input-small" bs-typeahead="fields.list" ng-model="panel.field" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small">Length</label>
|
||||
<input class="input-small" type="number" ng-model="panel.size" ng-change="set_refresh(true)">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small">Order</label>
|
||||
<select class="input-medium" ng-model="panel.order" ng-options="f for f in ['count','term','reverse_count','reverse_term']" ng-change="set_refresh(true)"></select></span>
|
||||
</div>
|
||||
<div class="span4">
|
||||
<label class="small">Exclude Terms(s) (comma seperated)</label>
|
||||
<input array-join type="text" ng-model='panel.exclude'></input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<div class="span2">
|
||||
<label class="small">Style</label>
|
||||
<select class="input-small" ng-model="panel.chart" ng-options="f for f in ['bar','pie','table']"></select></span>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.chart == 'table'">
|
||||
<label class="small">Font Size</label>
|
||||
<select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['7pt','8pt','9pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select></span>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.chart == 'bar' || panel.chart == 'pie'">
|
||||
<label class="small">Legend</label>
|
||||
<select class="input-small" ng-model="panel.counter_pos" ng-options="f for f in ['above','below','none']"></select></span>
|
||||
</div>
|
||||
<div class="span3" ng-show="panel.chart != 'table' && panel.counter_pos != 'none'">
|
||||
<label class="small" >Legend Format</label>
|
||||
<select class="input-small" ng-model="panel.arrangement" ng-options="f for f in ['horizontal','vertical']"></select></span>
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small">Missing</label><input type="checkbox" ng-model="panel.missing" ng-checked="panel.missing">
|
||||
</div>
|
||||
<div class="span1">
|
||||
<label class="small">Other</label><input type="checkbox" ng-model="panel.other" ng-checked="panel.other">
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Donut</label><input type="checkbox" ng-model="panel.donut" ng-checked="panel.donut">
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Tilt</label><input type="checkbox" ng-model="panel.tilt" ng-checked="panel.tilt">
|
||||
</div>
|
||||
<div class="span1" ng-show="panel.chart == 'pie'">
|
||||
<label class="small">Labels</label><input type="checkbox" ng-model="panel.labels" ng-checked="panel.labels">
|
||||
</div>
|
||||
</div>
|
||||
Executable
+58
@@ -0,0 +1,58 @@
|
||||
<div ng-controller='terms' ng-init="init()">
|
||||
<span ng-show="panel.spyable" class='spy panelextra pointer'>
|
||||
<i bs-modal="'app/partials/inspector.html'" class="icon-eye-open"></i>
|
||||
</span>
|
||||
<!-- START Pie or bar chart -->
|
||||
<div ng-show="panel.counter_pos == 'above' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
|
||||
<!-- vertical legend above -->
|
||||
<table class="small" ng-show="panel.arrangement == 'vertical'">
|
||||
<tr ng-repeat="term in legend">
|
||||
<td><i class="icon-circle" ng-style="{color:term.color}"></i></td> <td style="padding-right:10px;padding-left:10px;">{{term.label}}</td><td>{{term.data[0][1]}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- horizontal legend above -->
|
||||
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="term in legend" style="float:left;padding-left: 10px;">
|
||||
<span><i class="icon-circle" ng-style="{color:term.color}"></i> {{term.label}} ({{term.data[0][1]}}) </span>
|
||||
</div><br>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- keep legend from over lapping -->
|
||||
<div style="clear:both"></div>
|
||||
|
||||
<div ng-show="panel.chart == 'pie' || panel.chart == 'bar'" terms-chart params="{{panel}}" style="position:relative" class="pointer"></div>
|
||||
|
||||
<div ng-show="panel.counter_pos == 'below' && (panel.chart == 'bar' || panel.chart == 'pie')" id='{{$id}}-legend'>
|
||||
<!-- vertical legend below -->
|
||||
<table class="small" ng-show="panel.arrangement == 'vertical'">
|
||||
<tr ng-repeat="term in legend">
|
||||
<td><i class="icon-circle" ng-style="{color:term.color}"></i></i></td> <td style="padding-right:10px;padding-left:10px;">{{term.label}}</td><td>{{term.data[0][1]}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<!-- horizontal legend below -->
|
||||
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="term in legend" style="float:left;padding-left: 10px;">
|
||||
<span><i class="icon-circle" ng-style="{color:term.color}"></i></span> {{term.label}} ({{term.data[0][1]}}) </span>
|
||||
</div><br>
|
||||
|
||||
</div>
|
||||
<!-- END Pie or Bar chart -->
|
||||
|
||||
<table ng-style="panel.style" class="table table-striped table-condensed" ng-show="panel.chart == 'table'">
|
||||
<thead>
|
||||
<th>Term</th> <th>Count</th> <th>Action</th>
|
||||
</thead>
|
||||
<tr ng-repeat="term in data" ng-show="showMeta(term)">
|
||||
<td>{{term.label}}</td>
|
||||
<td>{{term.data[0][1]}}</td>
|
||||
<td>
|
||||
<span ng-hide="term.meta == 'other'">
|
||||
<i class='icon-search pointer' ng-click="build_search(term)"></i>
|
||||
<i class='icon-ban-circle pointer' ng-click="build_search(term,true)"></i>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
Executable
+299
@@ -0,0 +1,299 @@
|
||||
/*
|
||||
## Terms
|
||||
|
||||
### Parameters
|
||||
* style :: A hash of css styles
|
||||
* size :: top N
|
||||
* arrangement :: How should I arrange the query results? 'horizontal' or 'vertical'
|
||||
* chart :: Show a chart? 'none', 'bar', 'pie'
|
||||
* donut :: Only applies to 'pie' charts. Punches a hole in the chart for some reason
|
||||
* tilt :: Only 'pie' charts. Janky 3D effect. Looks terrible 90% of the time.
|
||||
* lables :: Only 'pie' charts. Labels on the pie?
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _, $, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.terms', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('terms', function($scope, querySrv, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
editorTabs : [
|
||||
{title:'Queries', src:'app/partials/querySelect.html'}
|
||||
],
|
||||
status : "Beta",
|
||||
description : "Displays the results of an elasticsearch facet as a pie chart, bar chart, or a "+
|
||||
"table"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
queries : {
|
||||
mode : 'all',
|
||||
ids : []
|
||||
},
|
||||
field : '_type',
|
||||
exclude : [],
|
||||
missing : true,
|
||||
other : true,
|
||||
size : 10,
|
||||
order : 'count',
|
||||
style : { "font-size": '10pt'},
|
||||
donut : false,
|
||||
tilt : false,
|
||||
labels : true,
|
||||
arrangement : 'horizontal',
|
||||
chart : 'bar',
|
||||
counter_pos : 'above',
|
||||
spyable : true
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function () {
|
||||
$scope.hits = 0;
|
||||
|
||||
$scope.$on('refresh',function(){
|
||||
$scope.get_data();
|
||||
});
|
||||
$scope.get_data();
|
||||
|
||||
};
|
||||
|
||||
$scope.get_data = function() {
|
||||
// Make sure we have everything for the request to complete
|
||||
if(dashboard.indices.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.panelMeta.loading = true;
|
||||
var request,
|
||||
results,
|
||||
boolQuery;
|
||||
|
||||
request = $scope.ejs.Request().indices(dashboard.indices);
|
||||
|
||||
$scope.panel.queries.ids = querySrv.idsByMode($scope.panel.queries);
|
||||
// This could probably be changed to a BoolFilter
|
||||
boolQuery = $scope.ejs.BoolQuery();
|
||||
_.each($scope.panel.queries.ids,function(id) {
|
||||
boolQuery = boolQuery.should(querySrv.getEjsObj(id));
|
||||
});
|
||||
|
||||
// Terms mode
|
||||
request = request
|
||||
.facet($scope.ejs.TermsFacet('terms')
|
||||
.field($scope.panel.field)
|
||||
.size($scope.panel.size)
|
||||
.order($scope.panel.order)
|
||||
.exclude($scope.panel.exclude)
|
||||
.facetFilter($scope.ejs.QueryFilter(
|
||||
$scope.ejs.FilteredQuery(
|
||||
boolQuery,
|
||||
filterSrv.getBoolFilter(filterSrv.ids)
|
||||
)))).size(0);
|
||||
|
||||
// Populate the inspector panel
|
||||
$scope.inspector = angular.toJson(JSON.parse(request.toString()),true);
|
||||
|
||||
results = request.doSearch();
|
||||
|
||||
// Populate scope when we have results
|
||||
results.then(function(results) {
|
||||
var k = 0;
|
||||
$scope.panelMeta.loading = false;
|
||||
$scope.hits = results.hits.total;
|
||||
$scope.data = [];
|
||||
_.each(results.facets.terms.terms, function(v) {
|
||||
var slice = { label : v.term, data : [[k,v.count]], actions: true};
|
||||
$scope.data.push(slice);
|
||||
k = k + 1;
|
||||
});
|
||||
|
||||
$scope.data.push({label:'Missing field',
|
||||
data:[[k,results.facets.terms.missing]],meta:"missing",color:'#aaa',opacity:0});
|
||||
$scope.data.push({label:'Other values',
|
||||
data:[[k+1,results.facets.terms.other]],meta:"other",color:'#444'});
|
||||
|
||||
$scope.$emit('render');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.build_search = function(term,negate) {
|
||||
if(_.isUndefined(term.meta)) {
|
||||
filterSrv.set({type:'terms',field:$scope.panel.field,value:term.label,
|
||||
mandate:(negate ? 'mustNot':'must')});
|
||||
} else if(term.meta === 'missing') {
|
||||
filterSrv.set({type:'exists',field:$scope.panel.field,
|
||||
mandate:(negate ? 'must':'mustNot')});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
dashboard.refresh();
|
||||
};
|
||||
|
||||
$scope.set_refresh = function (state) {
|
||||
$scope.refresh = state;
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
if($scope.refresh) {
|
||||
$scope.get_data();
|
||||
}
|
||||
$scope.refresh = false;
|
||||
$scope.$emit('render');
|
||||
};
|
||||
|
||||
$scope.showMeta = function(term) {
|
||||
if(_.isUndefined(term.meta)) {
|
||||
return true;
|
||||
}
|
||||
if(term.meta === 'other' && !$scope.panel.other) {
|
||||
return false;
|
||||
}
|
||||
if(term.meta === 'missing' && !$scope.panel.missing) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('termsChart', function(querySrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
|
||||
// Receive render events
|
||||
scope.$on('render',function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Re-render if the window is resized
|
||||
angular.element(window).bind('resize', function(){
|
||||
render_panel();
|
||||
});
|
||||
|
||||
// Function for rendering panel
|
||||
function render_panel() {
|
||||
var plot, chartData;
|
||||
|
||||
// IE doesn't work without this
|
||||
elem.css({height:scope.panel.height||scope.row.height});
|
||||
|
||||
// Make a clone we can operate on.
|
||||
chartData = _.clone(scope.data);
|
||||
chartData = scope.panel.missing ? chartData :
|
||||
_.without(chartData,_.findWhere(chartData,{meta:'missing'}));
|
||||
chartData = scope.panel.other ? chartData :
|
||||
_.without(chartData,_.findWhere(chartData,{meta:'other'}));
|
||||
|
||||
// Populate element.
|
||||
require(['jquery.flot.pie'], function(){
|
||||
// Populate element
|
||||
try {
|
||||
// Add plot to scope so we can build out own legend
|
||||
if(scope.panel.chart === 'bar') {
|
||||
plot = $.plot(elem, chartData, {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
lines: { show: false, },
|
||||
bars: { show: true, fill: 1, barWidth: 0.8, horizontal: false },
|
||||
shadowSize: 1
|
||||
},
|
||||
yaxis: { show: true, min: 0, color: "#c8c8c8" },
|
||||
xaxis: { show: false },
|
||||
grid: {
|
||||
borderWidth: 0,
|
||||
borderColor: '#eee',
|
||||
color: "#eee",
|
||||
hoverable: true,
|
||||
clickable: true
|
||||
},
|
||||
colors: querySrv.colors
|
||||
});
|
||||
}
|
||||
if(scope.panel.chart === 'pie') {
|
||||
var labelFormat = function(label, series){
|
||||
return '<div ng-click="build_search(panel.field,\''+label+'\')'+
|
||||
' "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
|
||||
label+'<br/>'+Math.round(series.percent)+'%</div>';
|
||||
};
|
||||
|
||||
plot = $.plot(elem, chartData, {
|
||||
legend: { show: false },
|
||||
series: {
|
||||
pie: {
|
||||
innerRadius: scope.panel.donut ? 0.4 : 0,
|
||||
tilt: scope.panel.tilt ? 0.45 : 1,
|
||||
radius: 1,
|
||||
show: true,
|
||||
combine: {
|
||||
color: '#999',
|
||||
label: 'The Rest'
|
||||
},
|
||||
stroke: {
|
||||
width: 0
|
||||
},
|
||||
label: {
|
||||
show: scope.panel.labels,
|
||||
radius: 2/3,
|
||||
formatter: labelFormat,
|
||||
threshold: 0.1
|
||||
}
|
||||
}
|
||||
},
|
||||
//grid: { hoverable: true, clickable: true },
|
||||
grid: { hoverable: true, clickable: true },
|
||||
colors: querySrv.colors
|
||||
});
|
||||
}
|
||||
|
||||
// Populate legend
|
||||
if(elem.is(":visible")){
|
||||
setTimeout(function(){
|
||||
scope.legend = plot.getData();
|
||||
if(!scope.$$phase) {
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
} catch(e) {
|
||||
elem.text(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
elem.bind("plotclick", function (event, pos, object) {
|
||||
if(object) {
|
||||
scope.build_search(scope.data[object.seriesIndex]);
|
||||
}
|
||||
});
|
||||
|
||||
var $tooltip = $('<div>');
|
||||
elem.bind("plothover", function (event, pos, item) {
|
||||
if (item) {
|
||||
var value = scope.panel.chart === 'bar' ? item.datapoint[1] : item.datapoint[1][0][1];
|
||||
$tooltip
|
||||
.html(
|
||||
kbn.query_color_dot(item.series.color, 20) + ' ' +
|
||||
item.series.label + " (" + value.toFixed(0)+")"
|
||||
)
|
||||
.place_tt(pos.pageX, pos.pageY);
|
||||
} else {
|
||||
$tooltip.remove();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
});
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
<div>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
<label class="small">Mode</label> <select class="input-medium" ng-model="panel.mode" ng-options="f for f in ['html','markdown','text']"></select>
|
||||
</div>
|
||||
<div class="span2" ng-show="panel.mode == 'text'">
|
||||
<label class="small">Font Size</label> <select class="input-mini" ng-model="panel.style['font-size']" ng-options="f for f in ['6pt','7pt','8pt','10pt','12pt','14pt','16pt','18pt','20pt','24pt','28pt','32pt','36pt','42pt','48pt','52pt','60pt','72pt']"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class=small>Content
|
||||
<span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
|
||||
<span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
|
||||
</label>
|
||||
<textarea ng-model="panel.content" rows="6" style="width:95%"></textarea>
|
||||
</div>
|
||||
Executable
+1454
File diff suppressed because it is too large
Load Diff
Executable
+10
@@ -0,0 +1,10 @@
|
||||
<div ng-controller='text' ng-init="init()">
|
||||
<!--<p ng-style="panel.style" ng-bind-html-unsafe="panel.content | striphtml | newlines"></p>-->
|
||||
<markdown ng-show="ready && panel.mode == 'markdown'">
|
||||
{{panel.content}}
|
||||
</markdown>
|
||||
<p ng-show="panel.mode == 'text'" ng-style='panel.style' ng-bind-html="panel.content | striphtml | newlines">
|
||||
</p>
|
||||
<p ng-show="panel.mode == 'html'" ng-bind-html="panel.content">
|
||||
</p>
|
||||
</div>
|
||||
Executable
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
## Text
|
||||
### Parameters
|
||||
* mode :: 'text', 'html', 'markdown'
|
||||
* content :: Content of the panel
|
||||
* style :: Hash containing css properties
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'require'
|
||||
],
|
||||
function (angular, app, _, require) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.text', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('text', function($scope) {
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
|
||||
};
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
status : "Stable",
|
||||
mode : "markdown",
|
||||
content : "",
|
||||
style: {},
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
$scope.ready = false;
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
module.directive('markdown', function() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
link: function(scope, element) {
|
||||
scope.$on('render', function() {
|
||||
render_panel();
|
||||
});
|
||||
|
||||
function render_panel() {
|
||||
require(['./lib/showdown'], function (Showdown) {
|
||||
scope.ready = true;
|
||||
var converter = new Showdown.converter();
|
||||
var text = scope.panel.content.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
var htmlText = converter.makeHtml(text);
|
||||
element.html(htmlText);
|
||||
// For whatever reason, this fixes chrome. I don't like it, I think
|
||||
// it makes things slow?
|
||||
if(!scope.$$phase) {
|
||||
scope.$apply();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
render_panel();
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('newlines', function(){
|
||||
return function (input) {
|
||||
return input.replace(/\n/g, '<br/>');
|
||||
};
|
||||
});
|
||||
|
||||
module.filter('striphtml', function () {
|
||||
return function(text) {
|
||||
return text
|
||||
.replace(/&/g, '&')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/</g, '<');
|
||||
};
|
||||
});
|
||||
});
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
<div class="row-fluid">
|
||||
<div class="span3">
|
||||
<h6>Default Mode</h6>
|
||||
<select style="width:85%" ng-model="panel.mode" ng-options="f for f in ['relative','absolute','since']"></select>
|
||||
</div>
|
||||
<div class="span3">
|
||||
<h6>Time Field</h6>
|
||||
<input type="text" class="input-small" ng-model="panel.timefield">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<h5>Relative mode <small>settings</small></h5>
|
||||
<div class="span6">
|
||||
<h6>Relative time options <small>comma seperated</small></h6>
|
||||
<input type="text" array-join class="input-large" ng-model="panel.time_options">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<h6>Default timespan</h6>
|
||||
<select class="input-mini" ng-model="panel.timespan" ng-options="f for f in panel.time_options"></select>
|
||||
</div>
|
||||
<div class="row-fluid">
|
||||
<h5>Auto-refresh <small>settings</small></h5>
|
||||
<div class="span1">
|
||||
<label class="small"> Enable </label><input type="checkbox" ng-model="panel.refresh.enable" ng-checked="panel.refresh.enable">
|
||||
</div>
|
||||
<div class="span2">
|
||||
<label class="small"> Interval (seconds) </label>
|
||||
<input type="number" class="input-mini" ng-model="panel.refresh.interval">
|
||||
</div>
|
||||
<div class="span3">
|
||||
<label class="small"> Minimum Interval (seconds) </label>
|
||||
<input type="number" class="input-mini" ng-model="panel.refresh.min">
|
||||
</div>
|
||||
</div>
|
||||
Executable
+77
@@ -0,0 +1,77 @@
|
||||
<div ng-controller='timepicker' ng-init="init()">
|
||||
<style>
|
||||
.timepicker-block {
|
||||
display: inline-block;
|
||||
}
|
||||
</style>
|
||||
<div class="row-fluid form-horizontal" ng-switch="panel.mode" ng-show="filterSrv.idsByType('time').length > 0">
|
||||
<div ng-switch-when="absolute" >
|
||||
<div class="timepicker-block">
|
||||
<form class="nomargin">
|
||||
<label><small>From</small></label>
|
||||
<input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.from.date" data-date-format="mm/dd/yyyy" bs-datepicker>
|
||||
<input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.from.time" bs-timepicker>
|
||||
</form>
|
||||
</div>
|
||||
<div class="timepicker-block" style="margin-left:10px">
|
||||
<form class="nomargin">
|
||||
<label style="margin-left:5px"><small>To (<a ng-click="to_now()">now</a>)</small></label>
|
||||
<input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.to.date" data-date-format="mm/dd/yyyy" bs-datepicker>
|
||||
<input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.to.time" bs-timepicker>
|
||||
</form>
|
||||
</div>
|
||||
<div class="timepicker-block">
|
||||
<form class="nomargin">
|
||||
<button class="btn" ng-click="time_apply()"><i class="icon-ok"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="since">
|
||||
<div class="timepicker-block">
|
||||
<form class="nomargin">
|
||||
<label><small>Since</small></label>
|
||||
<input type="text" class="input-smaller" ng-change="time_check()" ng-model="timepicker.from.date" data-date-format="mm/dd/yyyy" bs-datepicker>
|
||||
<input type="text" class="input-mini" ng-change="time_check()" data-show-meridian="false" data-show-seconds="true" ng-model="timepicker.from.time" bs-timepicker>
|
||||
</form>
|
||||
</div>
|
||||
<div class="timepicker-block" style="margin-left:10px">
|
||||
<form class="nomargin">
|
||||
<label><small><br></small></label>
|
||||
<button class="btn" ng-click="time_apply()" ><i class="icon-ok"></i></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-switch-when="relative">
|
||||
<div class="timepicker-block">
|
||||
<form class="nomargin input-append">
|
||||
<label><small>The last</small></label>
|
||||
<button class="btn btn" ng-repeat='timespan in panel.time_options' ng-class="{'btn-success': (panel.timespan == timespan)}" ng-click="set_timespan(timespan)">{{timespan}}</button>
|
||||
<!--<select ng-model="panel.sort[0]" ng-options="f for f in fields"></select>-->
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid" ng-show="filterSrv.idsByType('time').length < 1">
|
||||
<div>
|
||||
<div class="span11">
|
||||
<h4>No time filter present</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-fluid nomargin">
|
||||
<div class="span12 small" ng-show="filterSrv.idsByType('time').length > 0">
|
||||
<a class="link" ng-click="set_mode('relative')" ng-class="{'strong': (panel.mode == 'relative')}">Relative</a> |
|
||||
<a class="link" ng-click="set_mode('absolute')" ng-class="{'strong': (panel.mode == 'absolute')}">Absolute</a> |
|
||||
<a class="link" ng-click="set_mode('since')" ng-class="{'strong': (panel.mode == 'since')}">Since</a>
|
||||
<span ng-hide="panel.mode == 'absolute' || panel.mode == 'none'"> |
|
||||
<input type="checkbox" ng-model="panel.refresh.enable" ng-change='refresh();'> Auto-refresh
|
||||
<span ng-class="{'ng-cloak': !panel.refresh.enable}">
|
||||
every <a data-title="<small>Auto-refresh Settings</small>" data-placement="bottom" bs-popover="'app/panels/timepicker/refreshctrl.html'">{{panel.refresh.interval}}s</a>.
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="span12 small" ng-show="filterSrv.idsByType('time').length < 1">
|
||||
<a class='btn btn-small' ng-click="time_apply()">Create a time filter</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Executable
+268
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
|
||||
## Timepicker
|
||||
|
||||
### Parameters
|
||||
* mode :: The default mode of the panel. Options: 'relative', 'absolute' 'since' Default: 'relative'
|
||||
* time_options :: An array of possible time options. Default: ['5m','15m','1h','6h','12h','24h','2d','7d','30d']
|
||||
* timespan :: The default options selected for the relative view. Default: '15m'
|
||||
* timefield :: The field in which time is stored in the document.
|
||||
* refresh: Object containing refresh parameters
|
||||
* enable :: true/false, enable auto refresh by default. Default: false
|
||||
* interval :: Seconds between auto refresh. Default: 30
|
||||
* min :: The lowest interval a user may set
|
||||
*/
|
||||
define([
|
||||
'angular',
|
||||
'app',
|
||||
'underscore',
|
||||
'moment',
|
||||
'kbn'
|
||||
],
|
||||
function (angular, app, _, moment, kbn) {
|
||||
'use strict';
|
||||
|
||||
var module = angular.module('kibana.panels.timepicker', []);
|
||||
app.useModule(module);
|
||||
|
||||
module.controller('timepicker', function($scope, $rootScope, $timeout, timer, $http, dashboard, filterSrv) {
|
||||
$scope.panelMeta = {
|
||||
status : "Stable",
|
||||
description : "A panel for controlling the time range filters. If you have time based data, "+
|
||||
" or if you're using time stamped indices, you need one of these"
|
||||
};
|
||||
|
||||
|
||||
// Set and populate defaults
|
||||
var _d = {
|
||||
status : "Stable",
|
||||
mode : "relative",
|
||||
time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
|
||||
timespan : '15m',
|
||||
timefield : '@timestamp',
|
||||
timeformat : "",
|
||||
refresh : {
|
||||
enable : false,
|
||||
interval: 30,
|
||||
min : 3
|
||||
}
|
||||
};
|
||||
_.defaults($scope.panel,_d);
|
||||
|
||||
$scope.init = function() {
|
||||
// Private refresh interval that we can use for view display without causing
|
||||
// unnecessary refreshes during changes
|
||||
$scope.refresh_interval = $scope.panel.refresh.interval;
|
||||
$scope.filterSrv = filterSrv;
|
||||
|
||||
// Init a private time object with Date() objects depending on mode
|
||||
switch($scope.panel.mode) {
|
||||
case 'absolute':
|
||||
$scope.time = {
|
||||
from : moment($scope.panel.time.from,'MM/DD/YYYY HH:mm:ss') || moment(kbn.time_ago($scope.panel.timespan)),
|
||||
to : moment($scope.panel.time.to,'MM/DD/YYYY HH:mm:ss') || moment()
|
||||
};
|
||||
break;
|
||||
case 'since':
|
||||
$scope.time = {
|
||||
from : moment($scope.panel.time.from,'MM/DD/YYYY HH:mm:ss') || moment(kbn.time_ago($scope.panel.timespan)),
|
||||
to : moment()
|
||||
};
|
||||
break;
|
||||
case 'relative':
|
||||
$scope.time = {
|
||||
from : moment(kbn.time_ago($scope.panel.timespan)),
|
||||
to : moment()
|
||||
};
|
||||
break;
|
||||
}
|
||||
$scope.time.field = $scope.panel.timefield;
|
||||
// These 3 statements basicly do everything time_apply() does
|
||||
set_timepicker($scope.time.from,$scope.time.to);
|
||||
update_panel();
|
||||
|
||||
// If we're in a mode where something must be calculated, clear existing filters
|
||||
// and set new ones
|
||||
if($scope.panel.mode !== 'absolute') {
|
||||
set_time_filter($scope.time);
|
||||
}
|
||||
|
||||
dashboard.refresh();
|
||||
|
||||
|
||||
// Start refresh timer if enabled
|
||||
if ($scope.panel.refresh.enable) {
|
||||
$scope.set_interval($scope.panel.refresh.interval);
|
||||
}
|
||||
|
||||
// In case some other panel broadcasts a time, set us to an absolute range
|
||||
$scope.$on('refresh', function() {
|
||||
if(filterSrv.idsByType('time').length > 0) {
|
||||
var time = filterSrv.timeRange('min');
|
||||
|
||||
if($scope.time.from.diff(moment.utc(time.from),'seconds') !== 0 ||
|
||||
$scope.time.to.diff(moment.utc(time.to),'seconds') !== 0)
|
||||
{
|
||||
$scope.set_mode('absolute');
|
||||
|
||||
// These 3 statements basicly do everything time_apply() does
|
||||
set_timepicker(moment(time.from),moment(time.to));
|
||||
$scope.time = $scope.time_calc();
|
||||
update_panel();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
$scope.set_interval = function (refresh_interval) {
|
||||
$scope.panel.refresh.interval = refresh_interval;
|
||||
if(_.isNumber($scope.panel.refresh.interval)) {
|
||||
if($scope.panel.refresh.interval < $scope.panel.refresh.min) {
|
||||
$scope.panel.refresh.interval = $scope.panel.refresh.min;
|
||||
timer.cancel($scope.refresh_timer);
|
||||
return;
|
||||
}
|
||||
timer.cancel($scope.refresh_timer);
|
||||
$scope.refresh();
|
||||
} else {
|
||||
timer.cancel($scope.refresh_timer);
|
||||
}
|
||||
};
|
||||
|
||||
$scope.refresh = function() {
|
||||
if ($scope.panel.refresh.enable) {
|
||||
timer.cancel($scope.refresh_timer);
|
||||
$scope.refresh_timer = timer.register($timeout(function() {
|
||||
$scope.refresh();
|
||||
$scope.time_apply();
|
||||
},$scope.panel.refresh.interval*1000));
|
||||
} else {
|
||||
timer.cancel($scope.refresh_timer);
|
||||
}
|
||||
};
|
||||
|
||||
var update_panel = function() {
|
||||
// Update panel's string representation of the time object.Don't update if
|
||||
// we're in relative mode since we dont want to store the time object in the
|
||||
// json for relative periods
|
||||
if($scope.panel.mode !== 'relative') {
|
||||
$scope.panel.time = {
|
||||
from : $scope.time.from.format("MM/DD/YYYY HH:mm:ss"),
|
||||
to : $scope.time.to.format("MM/DD/YYYY HH:mm:ss"),
|
||||
};
|
||||
} else {
|
||||
delete $scope.panel.time;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.set_mode = function(mode) {
|
||||
$scope.panel.mode = mode;
|
||||
$scope.panel.refresh.enable = mode === 'absolute' ?
|
||||
false : $scope.panel.refresh.enable;
|
||||
|
||||
update_panel();
|
||||
};
|
||||
|
||||
$scope.to_now = function() {
|
||||
$scope.timepicker.to = {
|
||||
time : moment().format("HH:mm:ss"),
|
||||
date : moment().format("MM/DD/YYYY")
|
||||
};
|
||||
};
|
||||
|
||||
$scope.set_timespan = function(timespan) {
|
||||
$scope.panel.timespan = timespan;
|
||||
$scope.timepicker.from = {
|
||||
time : moment(kbn.time_ago(timespan)).format("HH:mm:ss"),
|
||||
date : moment(kbn.time_ago(timespan)).format("MM/DD/YYYY")
|
||||
};
|
||||
$scope.time_apply();
|
||||
};
|
||||
|
||||
$scope.close_edit = function() {
|
||||
$scope.time_apply();
|
||||
};
|
||||
|
||||
//
|
||||
$scope.time_calc = function(){
|
||||
var from,to;
|
||||
// If time picker is defined (usually is) TOFIX: Horrible parsing
|
||||
if(!(_.isUndefined($scope.timepicker))) {
|
||||
from = $scope.panel.mode === 'relative' ? moment(kbn.time_ago($scope.panel.timespan)) :
|
||||
moment(moment($scope.timepicker.from.date).format('MM/DD/YYYY') + " " + $scope.timepicker.from.time,'MM/DD/YYYY HH:mm:ss');
|
||||
to = $scope.panel.mode !== 'absolute' ? moment() :
|
||||
moment(moment($scope.timepicker.to.date).format('MM/DD/YYYY') + " " + $scope.timepicker.to.time,'MM/DD/YYYY HH:mm:ss');
|
||||
// Otherwise (probably initialization)
|
||||
} else {
|
||||
from = $scope.panel.mode === 'relative' ? moment(kbn.time_ago($scope.panel.timespan)) :
|
||||
$scope.time.from;
|
||||
to = $scope.panel.mode !== 'absolute' ? moment() :
|
||||
$scope.time.to;
|
||||
}
|
||||
|
||||
if (from.valueOf() >= to.valueOf()) {
|
||||
from = moment(to.valueOf() - 1000);
|
||||
}
|
||||
|
||||
$timeout(function(){
|
||||
set_timepicker(from,to);
|
||||
});
|
||||
|
||||
return {
|
||||
from : from,
|
||||
to : to
|
||||
};
|
||||
};
|
||||
|
||||
$scope.time_apply = function() {
|
||||
$scope.panel.error = "";
|
||||
// Update internal time object
|
||||
|
||||
// Remove all other time filters
|
||||
filterSrv.removeByType('time');
|
||||
|
||||
|
||||
$scope.time = $scope.time_calc();
|
||||
$scope.time.field = $scope.panel.timefield;
|
||||
|
||||
update_panel();
|
||||
set_time_filter($scope.time);
|
||||
|
||||
dashboard.refresh();
|
||||
|
||||
};
|
||||
$scope.$watch('panel.mode', $scope.time_apply);
|
||||
|
||||
function set_time_filter(time) {
|
||||
time.type = 'time';
|
||||
// Clear all time filters, set a new one
|
||||
filterSrv.removeByType('time');
|
||||
$scope.panel.filter_id = filterSrv.set(compile_time(time));
|
||||
return $scope.panel.filter_id;
|
||||
}
|
||||
|
||||
// Prefer to pass around Date() objects since interacting with
|
||||
// moment objects in libraries that are expecting Date()s can be tricky
|
||||
function compile_time(time) {
|
||||
time = _.clone(time);
|
||||
time.from = time.from.toDate();
|
||||
time.to = time.to.toDate();
|
||||
return time;
|
||||
}
|
||||
|
||||
function set_timepicker(from,to) {
|
||||
// Janky 0s timeout to get around $scope queue processing view issue
|
||||
$scope.timepicker = {
|
||||
from : {
|
||||
time : from.format("HH:mm:ss"),
|
||||
date : from.format("MM/DD/YYYY")
|
||||
},
|
||||
to : {
|
||||
time : to.format("HH:mm:ss"),
|
||||
date : to.format("MM/DD/YYYY")
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
Executable
+5
@@ -0,0 +1,5 @@
|
||||
<form name="refreshPopover" class='form-inline input-append' style="margin:0px">
|
||||
<label><small>Interval (seconds)</small></label><br>
|
||||
<input type="number" class="input-mini" ng-model="refresh_interval">
|
||||
<button type="button" class="btn" ng-click="set_interval(refresh_interval);dismiss()"><i class="icon-ok"></i></button>
|
||||
</form>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user