diff --git a/Gruntfile.js b/Gruntfile.js
index 1d640ff229bc0..3ff4a5200059b 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -15,7 +15,7 @@ module.exports = function (grunt) {
' Licensed <%= pkg.license %> */\n\n'
},
jshint: {
- files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js' ],
+ files: ['Gruntfile.js', 'js/*.js', 'panels/*/*.js', 'dashboards/*.js' ],
options: {
jshintrc: '.jshintrc'
}
diff --git a/dashboards/logstash.js b/dashboards/logstash.js
new file mode 100644
index 0000000000000..fb0edd2b3c758
--- /dev/null
+++ b/dashboards/logstash.js
@@ -0,0 +1,174 @@
+/*
+ * 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
+ },
+ {
+ type: 'dashcontrol',
+ span: 3
+ }
+];
+
+// 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(',') : ['@timestamp','@message'],
+ sort: !_.isUndefined(ARGS.sort) ? ARGS.sort.split(',') : [ARGS.timefield||'@timestamp','desc'],
+ overflow: 'expand'
+ }
+];
+
+// Now return the object and we're good!
+return dashboard;
diff --git a/dashboards/logstash.json b/dashboards/logstash.json
index 53ddc57d4a6cb..ead4dcac67a86 100644
--- a/dashboards/logstash.json
+++ b/dashboards/logstash.json
@@ -3,14 +3,11 @@
"services": {
"query": {
"idQueue": [
- 1,
- 2,
- 3,
- 4
+ 1
],
"list": {
"0": {
- "query": "*",
+ "query": "{{ARGS.query || '*'}}",
"alias": "",
"color": "#7EB26D",
"id": 0
@@ -22,8 +19,7 @@
},
"filter": {
"idQueue": [
- 1,
- 2
+ 1
],
"list": {
"0": {
@@ -70,7 +66,7 @@
"7d",
"30d"
],
- "timespan": "1h",
+ "timespan": "{{ARGS.from || '1h'}}",
"timefield": "@timestamp",
"timeformat": "",
"refresh": {
@@ -246,4 +242,4 @@
"pattern": "[logstash-]YYYY.MM.DD",
"default": "NO_TIME_FILTER_OR_INDEX_PATTERN_NOT_MATCHED"
}
-}
\ No newline at end of file
+}
diff --git a/js/app.js b/js/app.js
index b812468c198cd..7a1903381c00a 100644
--- a/js/app.js
+++ b/js/app.js
@@ -48,10 +48,10 @@ labjs.wait(function(){
.when('/dashboard', {
templateUrl: 'partials/dashboard.html',
})
- .when('/dashboard/:type/:id', {
+ .when('/dashboard/:kbnType/:kbnId', {
templateUrl: 'partials/dashboard.html',
})
- .when('/dashboard/:type/:id/:params', {
+ .when('/dashboard/:kbnType/:kbnId/:params', {
templateUrl: 'partials/dashboard.html'
})
.otherwise({
diff --git a/js/services.js b/js/services.js
index c254735882763..372460d4a3b8b 100644
--- a/js/services.js
+++ b/js/services.js
@@ -252,6 +252,13 @@ angular.module('kibana.services', [])
ids : [],
});
+ // Defaults for query objects
+ var _query = {
+ query: '*',
+ alias: '',
+ pin: false,
+ type: 'lucene'
+ };
// For convenience
var ejs = ejsResource(config.elasticsearch);
var _q = dashboard.current.services.query;
@@ -275,6 +282,12 @@ angular.module('kibana.services', [])
self.list = dashboard.current.services.query.list;
self.ids = dashboard.current.services.query.ids;
+ // Check each query object, populate its defaults
+ _.each(self.list,function(query,id) {
+ _.defaults(query,_query);
+ query.color = colorAt(id);
+ });
+
if (self.ids.length === 0) {
self.set({});
}
@@ -290,16 +303,12 @@ angular.module('kibana.services', [])
return false;
}
} else {
- var _id = nextId();
- var _query = {
- query: '*',
- alias: '',
- color: colorAt(_id),
- pin: false,
- id: _id,
- type: 'lucene'
- };
+ var _id = query.id || nextId();
+ query.id = _id;
+ query.color = query.color || colorAt(_id);
_.defaults(query,_query);
+
+
self.list[_id] = query;
self.ids.push(_id);
return _id;
@@ -373,11 +382,13 @@ angular.module('kibana.services', [])
.service('filterSrv', function(dashboard, ejsResource) {
// Create an object to hold our service state on the dashboard
dashboard.current.services.filter = dashboard.current.services.filter || {};
- _.defaults(dashboard.current.services.filter,{
+
+ // Defaults for it
+ var _d = {
idQueue : [],
list : {},
ids : []
- });
+ };
// For convenience
var ejs = ejsResource(config.elasticsearch);
@@ -388,6 +399,9 @@ angular.module('kibana.services', [])
// Call this whenever we need to reload the important stuff
this.init = function() {
+ // Populate defaults
+ _.defaults(dashboard.current.services.filter,_d);
+
// Accessors
self.list = dashboard.current.services.filter.list;
self.ids = dashboard.current.services.filter.ids;
@@ -592,9 +606,9 @@ angular.module('kibana.services', [])
var route = function() {
// Is there a dashboard type and id in the URL?
- if(!(_.isUndefined($routeParams.type)) && !(_.isUndefined($routeParams.id))) {
- var _type = $routeParams.type;
- var _id = $routeParams.id;
+ if(!(_.isUndefined($routeParams.kbnType)) && !(_.isUndefined($routeParams.kbnId))) {
+ var _type = $routeParams.kbnType;
+ var _id = $routeParams.kbnId;
switch(_type) {
case ('elasticsearch'):
@@ -606,6 +620,9 @@ angular.module('kibana.services', [])
case ('file'):
self.file_load(_id);
break;
+ case('script'):
+ self.script_load(_id);
+ break;
default:
self.file_load('default.json');
}
@@ -642,9 +659,7 @@ angular.module('kibana.services', [])
if(self.current.failover) {
self.indices = [self.current.index.default];
} else {
- alertSrv.set('No indices matched','The pattern '+self.current.index.pattern+
- ' did not match any indices in your selected'+
- ' time range.','info',5000);
+
// Do not issue refresh if no indices match. This should be removed when panels
// properly understand when no indices are present
return false;
@@ -653,10 +668,14 @@ angular.module('kibana.services', [])
$rootScope.$broadcast('refresh');
});
} else {
- // This is not optimal, we should be getting the entire index list here, or at least every
- // index that possibly matches the pattern
- self.indices = [self.current.index.default];
- $rootScope.$broadcast('refresh');
+ if(self.current.failover) {
+ self.indices = [self.current.index.default];
+ $rootScope.$broadcast('refresh');
+ } else {
+ alertSrv.set("No time filter",
+ 'Timestamped indices are configured without a failover. Waiting for time filter.',
+ 'info',5000);
+ }
}
} else {
self.indices = [self.current.index.default];
@@ -665,6 +684,7 @@ angular.module('kibana.services', [])
};
this.dash_load = function(dashboard) {
+
// Cancel all timers
timer.cancel_all();
@@ -744,11 +764,32 @@ angular.module('kibana.services', [])
};
};
+ var renderTemplate = function(json,params) {
+ var _r;
+ _.templateSettings = {interpolate : /\{\{(.+?)\}\}/g};
+ var template = _.template(json);
+ var rendered = template({ARGS:params});
+
+ try {
+ _r = angular.fromJson(rendered);
+ } catch(e) {
+ _r = false;
+ }
+ return _r;
+ };
+
this.file_load = function(file) {
return $http({
url: "dashboards/"+file,
method: "GET",
+ transformResponse: function(response) {
+ return renderTemplate(response,$routeParams);
+ }
}).then(function(result) {
+ if(!result) {
+ return false;
+ }
+
var _dashboard = result.data;
_.defaults(_dashboard,_dash);
self.dash_load(_dashboard);
@@ -759,11 +800,13 @@ angular.module('kibana.services', [])
});
};
-
this.elasticsearch_load = function(type,id) {
return $http({
url: config.elasticsearch + "/" + config.kibana_index + "/"+type+"/"+id,
- method: "GET"
+ method: "GET",
+ transformResponse: function(response) {
+ return renderTemplate(angular.fromJson(response)['_source']['dashboard'],$routeParams);
+ }
}).error(function(data, status, headers, conf) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
@@ -774,7 +817,32 @@ angular.module('kibana.services', [])
}
return false;
}).success(function(data, status, headers) {
- self.dash_load(angular.fromJson(data['_source']['dashboard']));
+ self.dash_load(data);
+ });
+ };
+
+ this.script_load = function(file) {
+ return $http({
+ url: "dashboards/"+file,
+ method: "GET",
+ transformResponse: function(response) {
+ /*jshint -W054 */
+ var _f = new Function("ARGS",response);
+ return _f($routeParams);
+ }
+ }).then(function(result) {
+ if(!result) {
+ return false;
+ }
+ var _dashboard = result.data;
+ _.defaults(_dashboard,_dash);
+ self.dash_load(_dashboard);
+ return true;
+ },function(result) {
+ alertSrv.set('Error',
+ "Could not load scripts/"+file+". Please make sure it exists and returns a valid dashboard" ,
+ 'error');
+ return false;
});
};
diff --git a/panels/histogram/module.js b/panels/histogram/module.js
index 7a410dd610fa6..5bdbb2235be23 100644
--- a/panels/histogram/module.js
+++ b/panels/histogram/module.js
@@ -135,8 +135,6 @@ angular.module('kibana.histogram', [])
if(dashboard.indices.length === 0) {
return;
}
-
-
var _range = $scope.get_time_range();
var _interval = $scope.get_interval(_range);
@@ -177,6 +175,7 @@ angular.module('kibana.histogram', [])
// Then run it
var results = request.doSearch();
+
// Populate scope when we have results
results.then(function(results) {
$scope.panelMeta.loading = false;