Refactored panels to used dashboard, query and filter services and store them on the dashboard

pull/7/head
Rashid Khan 12 years ago
parent 0d560c24e6
commit 0d0d3f4a36
  1. 2
      common/css/main.css
  2. 3
      js/controllers.js
  3. 227
      js/services.js
  4. 60
      panels/bettermap/module.js
  5. 18
      panels/derivequeries/editor.html
  6. 29
      panels/derivequeries/module.html
  7. 56
      panels/derivequeries/module.js
  8. 4
      panels/fields/micropanel.html
  9. 14
      panels/fields/module.js
  10. 12
      panels/histogram/editor.html
  11. 8
      panels/histogram/module.html
  12. 103
      panels/histogram/module.js
  13. 23
      panels/hits/module.html
  14. 85
      panels/hits/module.js
  15. 21
      panels/map/module.html
  16. 59
      panels/map/module.js
  17. 125
      panels/pie/editor.html
  18. 106
      panels/pie/module.js
  19. 20
      panels/query/meta.html
  20. 2
      panels/query/module.js
  21. 39
      panels/table/module.js
  22. 34
      panels/timepicker/editor.html
  23. 121
      panels/timepicker/module.js
  24. 2
      panels/trends/module.html
  25. 33
      panels/trends/module.js
  26. 74
      partials/dasheditor.html

@ -123,8 +123,6 @@
max-width: 500px;
}
.popover-title { display: none; }
.tiny {
font-size: 50%;
}

@ -3,7 +3,8 @@
'use strict';
angular.module('kibana.controllers', [])
.controller('DashCtrl', function($scope, $rootScope, $http, $timeout, $route, ejsResource, eventBus, fields, dashboard) {
.controller('DashCtrl', function($scope, $rootScope, $http, $timeout, $route, ejsResource, eventBus,
fields, dashboard) {
var _d = {
title: "",

@ -84,6 +84,7 @@ angular.module('kibana.services', [])
})
.service('kbnIndex',function($http) {
// returns a promise containing an array of all indices matching the index
// pattern that exist in a given range
this.indices = function(from,to,pattern,interval) {
@ -233,14 +234,14 @@ angular.module('kibana.services', [])
var _query = {
query: '*',
alias: '',
color: colorAt(_id)
color: colorAt(_id),
id: _id
}
_.defaults(query,_query)
self.list[_id] = query;
self.ids.push(_id)
return id;
return _id;
}
}
this.remove = function(id) {
@ -256,6 +257,10 @@ angular.module('kibana.services', [])
}
}
this.findQuery = function(queryString) {
return _.findWhere(self.list,{query:queryString})
}
var nextId = function() {
if(_q.idQueue.length > 0) {
return _q.idQueue.shift()
@ -271,14 +276,185 @@ angular.module('kibana.services', [])
init();
})
.service('dashboard', function($routeParams, $http, $rootScope, ejsResource, timer) {
.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,{
idQueue : [],
list : {},
ids : [],
});
// For convenience
var ejs = ejsResource(config.elasticsearch);
var _f = dashboard.current.services.filter;
// Save a reference to this
var self = this;
// Accessors
this.list = dashboard.current.services.filter.list;
this.ids = dashboard.current.services.filter.ids;
// This is used both for adding filters and modifying them.
// If an id is passed, the filter at that id is updated
this.set = function(filter,id) {
_.defaults(filter,{mandate:'must'})
if(!_.isUndefined(id)) {
if(!_.isUndefined(self.list[id])) {
_.extend(self.list[id],filter);
return id;
} else {
return false;
}
} else {
if(_.isUndefined(filter.type)) {
return false;
} else {
var _id = nextId();
var _filter = {
alias: '',
id: _id
}
_.defaults(filter,_filter)
self.list[_id] = filter;
self.ids.push(_id)
return _id;
}
}
}
this.getBoolFilter = function(ids) {
// A default match all filter, just in case there are no other filters
var bool = ejs.BoolFilter().must(ejs.MatchAllFilter());
_.each(ids,function(id) {
switch(self.list[id].mandate)
{
case 'mustNot':
bool = bool.mustNot(self.getEjsObj(id));
break;
case 'should':
bool = bool.should(self.getEjsObj(id));
break;
default:
bool = bool.must(self.getEjsObj(id));
}
})
return bool;
}
this.getEjsObj = function(id) {
return self.toEjsObj(self.list[id])
}
this.toEjsObj = function (filter) {
switch(filter.type)
{
case 'time':
return ejs.RangeFilter(filter.field)
.from(filter.from)
.to(filter.to)
break;
case 'range':
return ejs.RangeFilter(filter.field)
.from(filter.from)
.to(filter.to)
break;
case 'querystring':
console.log(filter.query)
return ejs.QueryFilter(ejs.QueryStringQuery(filter.query))
break;
case 'terms':
return ejs.TermsFilter(filter.field,filter.value)
break;
case 'exists':
return ejs.ExistsFilter(filter.field)
break;
case 'missing':
return ejs.MissingFilter(filter.field)
break;
default:
return false;
}
}
this.getByType = function(type) {
return _.pick(self.list,self.idsByType(type))
}
this.removeByType = function(type) {
var ids = self.idsByType(type)
_.each(ids,function(id) {
self.remove(id)
})
return ids;
}
this.idsByType = function(type) {
return _.pluck(_.where(self.list,{type:type}),'id')
}
// This special function looks for all time filters, and returns a time range according to the mode
this.timeRange = function(mode) {
var _t = _.where(self.list,{type:'time'})
if(_t.length == 0) {
return false;
}
switch(mode) {
case "min":
return {
from: new Date(_.max(_.pluck(_t,'from'))),
to: new Date(_.min(_.pluck(_t,'to')))
}
break;
case "max":
return {
from: new Date(_.min(_.pluck(_t,'from'))),
to: new Date(_.max(_.pluck(_t,'to')))
}
break;
default:
return false;
}
}
this.remove = function(id) {
if(!_.isUndefined(self.list[id])) {
delete self.list[id];
// This must happen on the full path also since _.without returns a copy
self.ids = dashboard.current.services.filter.ids = _.without(self.ids,id)
_f.idQueue.unshift(id)
_f.idQueue.sort(function(a,b){return a-b});
return true;
} else {
return false;
}
}
var nextId = function() {
if(_f.idQueue.length > 0) {
return _f.idQueue.shift()
} else {
return self.ids.length;
}
}
})
.service('dashboard', function($routeParams, $http, $rootScope, $injector, ejsResource, timer, kbnIndex) {
// A hash of defaults to use when loading a dashboard
var _dash = {
title: "",
editable: true,
rows: [],
services: {}
services: {},
index: {
interval: 'none',
pattern: '_all',
default: '_all'
},
};
// An elasticJS client to use
@ -288,9 +464,11 @@ angular.module('kibana.services', [])
// Empty dashboard object
this.current = {};
this.last = {};
this.indices = [];
// Store a reference to this
var self = this;
var filterSrv,query;
$rootScope.$on('$routeChangeSuccess',function(){
route();
@ -326,6 +504,33 @@ angular.module('kibana.services', [])
}
}
// Since the dashboard is responsible for index computation, we can compute and assign the indices
// here before telling the panels to refresh
this.refresh = function() {
if(self.current.index.interval !== 'none') {
if(filterSrv.idsByType('time').length > 0) {
var _range = filterSrv.timeRange('min');
kbnIndex.indices(_range.from,_range.to,
self.current.index.pattern,self.current.index.interval
).then(function (p) {
if(p.length > 0) {
self.indices = p;
} else {
self.indices = [self.current.index.default]
}
$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]
}
} else {
self.indices = [self.current.index.pattern]
$rootScope.$broadcast('refresh')
}
}
this.to_file = function() {
var blob = new Blob([angular.toJson(self.current,true)], {type: "application/json;charset=utf-8"});
// from filesaver.js
@ -422,8 +627,6 @@ angular.module('kibana.services', [])
return false;
}
);
}
this.elasticsearch_delete = function(id) {
@ -497,8 +700,18 @@ angular.module('kibana.services', [])
}
this.dash_load = function(dashboard) {
if(dashboard.index.interval === 'none') {
self.indices = [dashboard.index.pattern]
}
self.current = dashboard;
timer.cancel_all();
// Ok, now that we've setup the current dashboard, we can inject our services
query = $injector.get('query');
filterSrv = $injector.get('filterSrv')
return true;
}

@ -12,17 +12,10 @@
* 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
### Group Events
#### Sends
* get_time :: On panel initialization get time range to query
#### Receives
* time :: An object containing the time range to use and the index(es) to query
* query :: An Array of queries, this panel uses only the first one
*/
angular.module('kibana.bettermap', [])
.controller('bettermap', function($scope, eventBus, query) {
.controller('bettermap', function($scope, eventBus, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -37,50 +30,53 @@ angular.module('kibana.bettermap', [])
_.defaults($scope.panel,_d)
$scope.init = function() {
$scope.$on('refresh',function(){
$scope.get_data();
})
eventBus.register($scope,'time', function(event,time){set_time(time)});
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
$scope.get_data();
}
$scope.get_data = function(segment,query_id) {
$scope.get_data = function(segment,query_id) {
$scope.panel.error = false;
// Make sure we have everything for the request to complete
if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
return
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
}
//$scope.panel.loading = true;
// 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.segment = _segment;
var boolQuery = ejs.BoolQuery();
_.each(query.list,function(q) {
boolQuery = boolQuery.should(ejs.QueryStringQuery((q.query || '*') + " AND _exists_:"+$scope.panel.field))
boolQuery = boolQuery.should(ejs.QueryStringQuery((q.query || '*')))
})
var request = $scope.ejs.Request().indices($scope.index[_segment])
var request = $scope.ejs.Request().indices(dashboard.indices[_segment])
.query(ejs.FilteredQuery(
boolQuery,
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
)
)
filterSrv.getBoolFilter(filterSrv.ids).must(ejs.ExistsFilter($scope.panel.field))
))
.fields([$scope.panel.field,$scope.panel.tooltip])
.size($scope.panel.size)
.sort($scope.time.field,'desc');
if(!_.isNull(timeField)) {
request = request.sort(timeField,'desc');
}
$scope.populate_modal(request)
@ -125,7 +121,7 @@ $scope.get_data = function(segment,query_id) {
$scope.$emit('draw')
// Get $size results then stop querying
if($scope.data.length < $scope.panel.size && _segment+1 < $scope.index.length)
if($scope.data.length < $scope.panel.size && _segment+1 < dashboard.indices.length)
$scope.get_data(_segment+1,$scope.query_id)
});
@ -136,18 +132,12 @@ $scope.get_data = function(segment,query_id) {
$scope.modal = {
title: "Inspector",
body : "<h5>Last Elasticsearch Query</h5><pre>"+
'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
angular.toJson(JSON.parse(request.toString()),true)+
"'</pre>",
}
}
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
})
.directive('bettermap', function() {
return {

@ -1,22 +1,26 @@
<div>
<div class="row-fluid">
<div class="span12">
The derive queries panel takes a query and a field, then runs a terms facet against both and generates a list of terms to query on. For example, you might want to see a histogram of the top 5 requests that return a 404. <strong>You should be careful not to select a high cardinality field</strong> as Elasticsearch must load all of these values into memory.<p>
Query Mode allows to optionally append original query to each term in the list.
</div>
</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="span8">
<div class="span5">
<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>
<div class="row-fluid">
<div class="span12">
The derive queries panel takes a query and a field, runs a terms facet, then creates queries based on them. For example, you might want to see a histogram of the top 5 requests that return a 404. <strong>You should be careful not to select a high cardinality field</strong> as Elasticsearch must load all of these values into memory.<p>
Query Mode allows to optionally append original query to each term in the list.
</div>
</div>
</div>

@ -1,8 +1,23 @@
<kibana-panel ng-controller='derivequeries' ng-init="init()">
<style>
.end-derive {
position:absolute;
right:15px;
top:5px;
}
.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="'partials/modal.html'" class="icon-eye-open"></i>
</span>
<div ng-show="!panel.multi">
<!--
<div>
<form>
<table class="form-horizontal">
<tr>
@ -29,4 +44,16 @@
</table>
</form>
</div>
-->
<label class="small">Create new queries from <strong>{{panel.field}}</strong> ({{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>
</kibana-panel>

@ -1,6 +1,6 @@
/*
## Termsquery
## Derivequeries
Broadcasts an array of queries based on the results of a terms facet
@ -23,13 +23,15 @@
*/
angular.module('kibana.derivequeries', [])
.controller('derivequeries', function($scope, eventBus) {
.controller('derivequeries', function($scope, $rootScope, query, eventBus, fields, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
loading : false,
status : "Beta",
label : "Search",
query : "*",
ids : [],
group : "default",
field : '_type',
fields : [],
@ -43,26 +45,19 @@ angular.module('kibana.derivequeries', [])
_.defaults($scope.panel,_d);
$scope.init = function() {
eventBus.register($scope,'fields', function(event, fields) {
$scope.panel.fields = fields.all;
});
eventBus.register($scope,'time', function(event,time){set_time(time)});
eventBus.register($scope,'query', function(event, query) {
$scope.panel.query = _.isArray(query) ? query[0] : query;
$scope.get_data();
});
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
$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(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
$scope.panel.loading = true;
var request = $scope.ejs.Request().indices($scope.index);
var request = $scope.ejs.Request().indices(dashboard.indices);
// Terms mode
request = request
@ -73,9 +68,7 @@ angular.module('kibana.derivequeries', [])
.facetFilter(ejs.QueryFilter(
ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
filterSrv.getBoolFilter(filterSrv.ids)
)))).size(0)
$scope.populate_modal(request);
@ -93,10 +86,22 @@ angular.module('kibana.derivequeries', [])
} else if ($scope.panel.mode === 'OR') {
var suffix = ' OR (' + $scope.panel.query + ')';
}
var ids = [];
_.each(results.facets.query.terms, function(v) {
data.push($scope.panel.field+':"'+v.term+'"'+suffix)
var _q = $scope.panel.field+':"'+v.term+'"'+suffix;
// if it isn't in the list, remove it
var _iq = query.findQuery(_q)
if(!_iq) {
ids.push(query.set({query:_q}));
} else {
ids.push(_iq.id);
}
});
$scope.send_query(data)
_.each(_.difference($scope.panel.ids,ids),function(id){
query.remove(id)
})
$scope.panel.ids = ids;
dashboard.refresh();
});
}
@ -114,23 +119,12 @@ angular.module('kibana.derivequeries', [])
$scope.modal = {
title: "Inspector",
body : "<h5>Last Elasticsearch Query</h5><pre>"+
'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
angular.toJson(JSON.parse(request.toString()),true)+
"'</pre>",
}
}
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
$scope.send_query = function(query) {
var _query = _.isArray(query) ? query : [query]
eventBus.broadcast($scope.$id,$scope.panel.group,'query',_query)
}
var update_history = function(query) {
query = _.isArray(query) ? query : [query];
if($scope.panel.remember > 0) {

@ -1,8 +1,8 @@
<a class="close" ng-click="dismiss()" href="">×</a>
<h4>
Micro Analysis of {{micropanel.field}}
<i class="pointer icon-search" ng-click="build_search('_exists_',micropanel.field);dismiss();"></i>
<i class="pointer icon-ban-circle" ng-click="build_search('_missing_',micropanel.field);dismiss();"></i>
<i class="pointer icon-search" ng-click="fieldExists(micropanel.field,'exists');dismiss();"></i>
<i class="pointer icon-ban-circle" ng-click="fieldExists(micropanel.field,'missing');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'>

@ -18,7 +18,7 @@
*/
angular.module('kibana.fields', [])
.controller('fields', function($scope, eventBus, $timeout) {
.controller('fields', function($scope, eventBus, $timeout, dashboard, query, filterSrv) {
// Set and populate defaults
var _d = {
@ -79,9 +79,15 @@ angular.module('kibana.fields', [])
eventBus.broadcast($scope.$id,$scope.panel.group,"selected_fields",$scope.active)
}
$scope.build_search = function(field, value,negate) {
$scope.panel.query = [add_to_query($scope.panel.query,field,value,negate)]
eventBus.broadcast($scope.$id,$scope.panel.group,'query',$scope.panel.query);
$scope.build_search = function(field,value,negate) {
var query = (negate ? '-':'+')+field+":\""+value+"\""
filterSrv.set({type:'querystring',query:query})
dashboard.refresh();
}
$scope.fieldExists = function(field,mode) {
filterSrv.set({type:mode,field:field})
dashboard.refresh();
}
$scope.is_active = function(field) {

@ -4,13 +4,19 @@
<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="span3" ng-show="panel.mode != 'count'">
<label class="small">Field</label>
<div class="span2">
<label class="small">Time Field</label>
<form>
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.time_field">
</form>
</div>
<div class="span2" ng-show="panel.mode != 'count'">
<label class="small">Value Field</label>
<form>
<input ng-change="set_refresh(true)" placeholder="Start typing" bs-typeahead="fields.list" type="text" class="input-small" ng-model="panel.value_field">
</form>
</div>
<div class="span5" ng-show="panel.mode != 'count'">
<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>

@ -22,12 +22,12 @@
</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(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 plot.getData()' class="histogram-legend">
<div class="histogram-legend-dot" style="background:{{series.color}};"></div>
<div class='small histogram-legend-item'>{{series.label}} ({{series.hits}})</div>
<span ng-show="panel.legend" ng-repeat='series in data' class="histogram-legend">
<div class="histogram-legend-dot" style="background:{{series.info.color}};"></div>
<div class='small histogram-legend-item'>{{series.info.alias}} ({{series.hits}})</div>
</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>

@ -42,7 +42,7 @@
*/
angular.module('kibana.histogram', [])
.controller('histogram', function($scope, eventBus,query) {
.controller('histogram', function($scope, eventBus, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -50,6 +50,7 @@ angular.module('kibana.histogram', [])
group : "default",
query : [ {query: "*", label:"Query"} ],
mode : 'count',
time_field : '@timestamp',
value_field : null,
auto_int : true,
resolution : 100,
@ -75,50 +76,46 @@ angular.module('kibana.histogram', [])
$scope.queries = query;
eventBus.register($scope,'time', function(event,time){$scope.set_time(time)});
$scope.$on('refresh',function(){
$scope.get_data();
})
// Now that we're all setup, request the time from our group if we don't
// have it yet
if(_.isUndefined($scope.time))
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
}
$scope.get_data = function(segment,query_id) {
delete $scope.panel.error
// Make sure we have everything for the request to complete
if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
var _range = $scope.range = filterSrv.timeRange('min');
if ($scope.panel.auto_int)
$scope.panel.interval = secondsToHms(calculate_interval($scope.time.from,$scope.time.to,$scope.panel.resolution,0)/1000);
$scope.panel.interval = secondsToHms(calculate_interval(_range.from,_range.to,$scope.panel.resolution,0)/1000);
$scope.panel.loading = true;
var _segment = _.isUndefined(segment) ? 0 : segment
var request = $scope.ejs.Request().indices($scope.index[_segment]);
var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
// Build the query
_.each($scope.queries.ids, function(id) {
var query = $scope.ejs.FilteredQuery(
ejs.QueryStringQuery($scope.queries.list[id].query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
filterSrv.getBoolFilter(filterSrv.ids)
)
var facet = $scope.ejs.DateHistogramFacet(id)
if($scope.panel.mode === 'count') {
facet = facet.field($scope.time.field)
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.time.field).valueField($scope.panel.value_field)
facet = facet.keyField($scope.panel.time_field).valueField($scope.panel.value_field)
}
facet = facet.interval($scope.panel.interval).facetFilter($scope.ejs.QueryFilter(query))
request = request.facet(facet).size(0)
@ -132,6 +129,7 @@ angular.module('kibana.histogram', [])
// Populate scope when we have results
results.then(function(results) {
$scope.panel.loading = false;
if(_segment == 0) {
$scope.hits = 0;
@ -145,15 +143,21 @@ angular.module('kibana.histogram', [])
return;
}
// Make sure we're still on the same query
if($scope.query_id === query_id) {
// Convert facet ids to numbers
var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k);})
// Make sure we're still on the same query/queries
if($scope.query_id === query_id &&
_.intersection(facetIds,query.ids).length == query.ids.length
) {
var i = 0;
_.each(results.facets, function(v, id) {
_.each(query.ids, function(id) {
var v = results.facets[id];
// Null values at each end of the time range ensure we see entire range
if(_.isUndefined($scope.data[i]) || _segment == 0) {
var data = [[$scope.time.from.getTime(), null],[$scope.time.to.getTime(), null]];
var data = [[_range.from.getTime(), null],[_range.to.getTime(), null]];
var hits = 0;
} else {
var data = $scope.data[i].data
@ -172,7 +176,7 @@ angular.module('kibana.histogram', [])
// Create the flot series object
var series = {
data: {
id: id,
info: $scope.queries.list[id],
data: data,
hits: hits
},
@ -187,7 +191,7 @@ angular.module('kibana.histogram', [])
$scope.$emit('render')
// If we still have segments left, get them
if(_segment < $scope.index.length-1) {
if(_segment < dashboard.indices.length-1) {
$scope.get_data(_segment+1,query_id)
}
@ -198,7 +202,32 @@ angular.module('kibana.histogram', [])
// function $scope.zoom
// factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
$scope.zoom = function(factor) {
eventBus.broadcast($scope.$id,$scope.panel.group,'zoom',factor);
var _now = Date.now();
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?
@ -223,14 +252,8 @@ angular.module('kibana.histogram', [])
$scope.$emit('render');
}
$scope.set_time = function(time) {
$scope.time = time;
$scope.index = time.index || $scope.index
$scope.get_data();
}
})
.directive('histogramChart', function(eventBus) {
.directive('histogramChart', function(dashboard, eventBus, filterSrv, $rootScope) {
return {
restrict: 'A',
link: function(scope, elem, attrs, ctrl) {
@ -249,10 +272,12 @@ angular.module('kibana.histogram', [])
function render_panel() {
// Populate from the query service
_.each(scope.data,function(series) {
series.label = scope.queries.list[series.id].alias,
series.color = scope.queries.list[series.id].color
})
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 = interval_to_seconds(scope.panel.interval)*1000
@ -366,9 +391,13 @@ angular.module('kibana.histogram', [])
});
elem.bind("plotselected", function (event, ranges) {
scope.time.from = moment(ranges.xaxis.from);
scope.time.to = moment(ranges.xaxis.to)
eventBus.broadcast(scope.$id,scope.panel.group,'set_time',scope.time)
var _id = filterSrv.set({
type : 'time',
from : moment.utc(ranges.xaxis.from),
to : moment.utc(ranges.xaxis.to),
field : scope.panel.time_field
})
dashboard.refresh();
});
}
};

@ -3,14 +3,14 @@
<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 plot.getData()">
<td><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.label}}</td><td>{{query.data[0][1]}}</td>
<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 plot.getData()" style="float:left;padding-left: 10px;">
<span><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></span> {{query.label}} ({{query.data[0][1]}}) </span>
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
<span><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></span> {{query.info.alias}} ({{query.data[0][1]}}) </span>
</div><br>
</div>
@ -20,23 +20,26 @@
<div ng-show="panel.chart == 'pie' || panel.chart == 'bar'" hits-chart params="{{panel}}" style="height:{{panel.height || row.height}};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 plot.getData()">
<td><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></td> <td style="padding-right:10px;padding-left:10px;">{{query.label}}</td><td>{{query.data[0][1]}}</td>
<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 plot.getData()" style="float:left;padding-left: 10px;">
<span><div style="display:inline-block;border-radius:5px;background:{{query.color}};height:10px;width:10px"></div></span> {{query.label}} ({{query.data[0][1]}}) </span>
<div class="small" ng-show="panel.arrangement == 'horizontal'" ng-repeat="query in data" style="float:left;padding-left: 10px;">
<span><div style="display:inline-block;border-radius:5px;background:{{query.info.color}};height:10px;width:10px"></div></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'"><span ng-style="panel.style" style="line-height:{{panel.style['font-size']}}" ng-repeat="query in data">{{query.label}} ({{query.hits}})<span></span><br ng-show="panel.arrangement == 'vertical' && panel.chart == 'list'">
<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'">
</kibana-panel>

@ -22,7 +22,7 @@
*/
angular.module('kibana.hits', [])
.controller('hits', function($scope, eventBus, query) {
.controller('hits', function($scope, eventBus, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -30,8 +30,8 @@ angular.module('kibana.hits', [])
query : ["*"],
group : "default",
style : { "font-size": '10pt'},
arrangement : 'vertical',
chart : 'none',
arrangement : 'horizontal',
chart : 'bar',
counter_pos : 'above',
donut : false,
tilt : false,
@ -40,22 +40,13 @@ angular.module('kibana.hits', [])
_.defaults($scope.panel,_d)
$scope.init = function () {
$scope.queries = query;
$scope.hits = 0;
eventBus.register($scope,'time', function(event,time){
set_time(time)
});
$scope.$on('refresh',function(){
console.log($scope.queries)
console.log(query)
$scope.get_data();
})
$scope.get_data();
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
}
$scope.get_data = function(segment,query_id) {
@ -63,23 +54,22 @@ angular.module('kibana.hits', [])
$scope.panel.loading = true;
// Make sure we have everything for the request to complete
if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
var _segment = _.isUndefined(segment) ? 0 : segment
var request = $scope.ejs.Request().indices($scope.index[_segment]);
var request = $scope.ejs.Request().indices(dashboard.indices[_segment]);
// Build the question part of the query
_.each($scope.queries.ids, function(id) {
var query = $scope.ejs.FilteredQuery(
ejs.QueryStringQuery($scope.queries.list[id].query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to))
_.each(query.ids, function(id) {
var _q = $scope.ejs.FilteredQuery(
ejs.QueryStringQuery(query.list[id].query || '*'),
filterSrv.getBoolFilter(filterSrv.ids));
request = request
.facet($scope.ejs.QueryFacet(id)
.query(query)
.query(_q)
).size(0)
});
@ -91,7 +81,6 @@ angular.module('kibana.hits', [])
// Populate scope when we have results
results.then(function(results) {
$scope.panel.loading = false;
if(_segment == 0) {
$scope.hits = 0;
@ -104,16 +93,24 @@ angular.module('kibana.hits', [])
$scope.panel.error = $scope.parse_error(results.error);
return;
}
if($scope.query_id === query_id) {
// Convert facet ids to numbers
var facetIds = _.map(_.keys(results.facets),function(k){return parseInt(k);})
// Make sure we're still on the same query/queries
if($scope.query_id === query_id &&
_.intersection(facetIds,query.ids).length == query.ids.length
) {
var i = 0;
_.each(results.facets, function(v, id) {
_.each(query.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] = {
//label: $scope.panel.query[i].label || "query"+(parseInt(i)+1),
info: query.list[id],
id: id,
hits: hits,
data: [[i,hits]]
@ -122,26 +119,13 @@ angular.module('kibana.hits', [])
i++;
});
$scope.$emit('render');
if(_segment < $scope.index.length-1)
if(_segment < dashboard.indices.length-1)
$scope.get_data(_segment+1,query_id)
}
});
}
$scope.remove_query = function(q) {
$scope.panel.query = _.without($scope.panel.query,q);
$scope.get_data();
}
$scope.add_query = function(label,query) {
$scope.panel.query.unshift({
query: query,
label: label,
});
$scope.get_data();
}
$scope.set_refresh = function (state) {
$scope.refresh = state;
}
@ -155,11 +139,10 @@ angular.module('kibana.hits', [])
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
}).directive('hitsChart', function(eventBus) {
}).directive('hitsChart', function(eventBus, query) {
return {
restrict: 'A',
link: function(scope, elem, attrs, ctrl) {
@ -177,10 +160,12 @@ angular.module('kibana.hits', [])
// Function for rendering panel
function render_panel() {
_.each(scope.data,function(series) {
series.label = scope.queries.list[series.id].alias,
series.color = scope.queries.list[series.id].color
})
try {
_.each(scope.data,function(series) {
series.label = series.info.alias,
series.color = series.info.color
})
} catch(e) {return}
var scripts = $LAB.script("common/lib/panels/jquery.flot.js").wait()
.script("common/lib/panels/jquery.flot.pie.js")
@ -201,13 +186,12 @@ angular.module('kibana.hits', [])
yaxis: { show: true, min: 0, color: "#c8c8c8" },
xaxis: { show: false },
grid: {
backgroundColor: '#272b30',
borderWidth: 0,
borderColor: '#eee',
color: "#eee",
hoverable: true,
},
colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
colors: query.colors
})
if(scope.panel.chart === 'pie')
scope.plot = $.plot(elem, scope.data, {
@ -223,7 +207,6 @@ angular.module('kibana.hits', [])
label: 'The Rest'
},
stroke: {
color: '#272b30',
width: 0
},
label: {
@ -239,7 +222,7 @@ angular.module('kibana.hits', [])
},
//grid: { hoverable: true, clickable: true },
grid: { hoverable: true, clickable: true },
colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
colors: query.colors
});
// Compensate for the height of the legend. Gross

@ -3,6 +3,7 @@
.jvectormap-label {
position: absolute;
display: none;
visibility: hidden;
border: solid 1px #CDCDCD;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
@ -30,6 +31,10 @@
text-align: center;
}
.jvectormap {
position: relative;
}
.jvectormap-zoomin {
display: none;
top: 10px;
@ -39,9 +44,23 @@
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="'partials/modal.html'" class="icon-eye-open"></i>
</span>
<div map params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
<div class="jvectormap" map params="{{panel}}" style="height:{{panel.height || row.height}}"></div>
</kibana-panel>

@ -28,7 +28,7 @@
*/
angular.module('kibana.map', [])
.controller('map', function($scope, eventBus) {
.controller('map', function($scope, $rootScope, eventBus, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -45,22 +45,24 @@ angular.module('kibana.map', [])
_.defaults($scope.panel,_d)
$scope.init = function() {
eventBus.register($scope,'time', function(event,time){set_time(time)});
eventBus.register($scope,'query', function(event, query) {
$scope.panel.query = _.isArray(query) ? query[0] : query;
$scope.get_data();
});
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
$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(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
$scope.panel.loading = true;
var request = $scope.ejs.Request().indices($scope.index);
var request = $scope.ejs.Request().indices(dashboard.indices);
var boolQuery = ejs.BoolQuery();
_.each(query.list,function(q) {
boolQuery = boolQuery.should(ejs.QueryStringQuery(q.query || '*'))
})
// Then the insert into facet and make the request
var request = request
@ -70,10 +72,8 @@ angular.module('kibana.map', [])
.exclude($scope.panel.exclude)
.facetFilter(ejs.QueryFilter(
ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
boolQuery,
filterSrv.getBoolFilter(filterSrv.ids)
)))).size(0);
$scope.populate_modal(request);
@ -97,22 +97,17 @@ angular.module('kibana.map', [])
$scope.modal = {
title: "Inspector",
body : "<h5>Last Elasticsearch Query</h5><pre>"+
'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
angular.toJson(JSON.parse(request.toString()),true)+
"'</pre>",
}
}
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
$scope.build_search = function(field,value) {
$scope.panel.query = add_to_query($scope.panel.query,field,value,false)
$scope.get_data();
eventBus.broadcast($scope.$id,$scope.panel.group,'query',[$scope.panel.query]);
_.each(query.list,function(q) {
q.query = add_to_query(q.query,field,value,false);
})
dashboard.refresh();
}
})
@ -155,20 +150,12 @@ angular.module('kibana.map', [])
}]
},
onRegionLabelShow: function(event, label, code){
$('.jvectormap-label').css({
"position" : "absolute",
"display" : "none",
'color' : "#c8c8c8",
'padding' : '10px',
'font-size' : '11pt',
'font-weight' : 200,
'background-color': '#1f1f1f',
'border-radius': '5px'
})
elem.children('.map-legend').show()
var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
$('.jvectormap-label').text(label.text() + ": " + count);
elem.children('.map-legend').text(label.text() + ": " + count);
},
onRegionOut: function(event, code) {
$('.map-legend').hide();
},
onRegionClick: function(event, code) {
var count = _.isUndefined(scope.data[code]) ? 0 : scope.data[code];
@ -176,6 +163,8 @@ angular.module('kibana.map', [])
scope.build_search(scope.panel.field,code)
}
});
elem.prepend('<span class="map-legend"></span>');
$('.map-legend').hide();
})
}
}

@ -1,83 +1,62 @@
<div class="row-fluid" ng-switch="panel.mode">
<div class="span3">
<label class="small">Mode</label>
<select class="input-small" ng-change="set_mode(panel.mode)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
</div>
<div ng-switch-when="terms">
<div>
<div class="row-fluid" ng-switch="panel.mode">
<div class="row-fluid">
<div class="span3">
<form style="margin-bottom: 0px">
<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" style="width:90%" bs-typeahead="fields.list" ng-model="panel.query.field">
</form>
</div>
<div class="span5">
<form class="input-append" style="margin-bottom: 0px">
<label class="small">Query</label>
<input type="text" style="width:80%" ng-model="panel.query.query">
<button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
</form>
</div>
</div>
<div class="row-fluid">
<div class="span3">
<label class="small">Length</label>
<input type="number" style="width:80%" ng-model="panel.size" ng-change="get_data()">
</div>
<div class="span8">
<form class="input-append" style="margin-bottom: 0px">
<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" style="width:90%" ng-model='panel.exclude'></input>
<button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
</form>
</div>
<input array-join type="text" ng-model='panel.exclude'></input>
</div>
</div>
</div>
</div>
<div ng-switch-when="goal">
<div class="row-fluid">
<div class="span3">
<label class="small">Mode</label>
<select class="input-small" ng-change="set_mode(panel.mode)" ng-model="panel.mode" ng-options="f for f in ['terms','goal']"></select>
</div>
<div class="span2">
<form style="margin-bottom: 0px">
<label class="small">Goal</label>
<input type="number" style="width:90%" ng-model="panel.query.goal">
</form>
</div>
<div class="span5">
<form class="input-append" style="margin-bottom: 0px">
<label class="small">Query</label>
<input type="text" style="width:80%" ng-model="panel.query.query">
<button class="btn" ng-click="get_data()"><i class="icon-search"></i></button>
</form>
<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>
<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>
<h5>Panel Spy</h5>
<div class="row-fluid">
<div class="span2">
<label class="small"> Spyable </label><input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
<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>
<div class="span9 small">
The panel spy shows 'behind the scenes' information about a panel. It can
be accessed by clicking the <i class='icon-eye-open'></i> in the top right
of the panel.
<h5>Panel Spy</h5>
<div class="row-fluid">
<div class="span2">
<label class="small"> Spyable </label><input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
</div>
<div class="span9 small">
The panel spy shows 'behind the scenes' information about a panel. It can
be accessed by clicking the <i class='icon-eye-open'></i> in the top right
of the panel.
</div>
</div>
</div>

@ -33,17 +33,17 @@
*/
angular.module('kibana.pie', [])
.controller('pie', function($scope, eventBus) {
.controller('pie', function($scope, $rootScope, eventBus, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
status : "Deprecating Soon",
query : { field:"_all", query:"*", goal: 1},
query : { field:"_type", goal: 100},
size : 10,
exclude : [],
donut : false,
tilt : false,
legend : true,
legend : "above",
labels : true,
mode : "terms",
group : "default",
@ -53,30 +53,7 @@ angular.module('kibana.pie', [])
_.defaults($scope.panel,_d)
$scope.init = function() {
eventBus.register($scope,'time', function(event,time){set_time(time)});
eventBus.register($scope,'query', function(event, query) {
$scope.panel.query.query = _.isArray(query) ? query[0] : query;
$scope.get_data();
});
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
}
$scope.remove_query = function(q) {
if($scope.panel.mode !== 'query')
return false;
$scope.panel.query = _.without($scope.panel.query,q);
$scope.get_data();
}
$scope.add_query = function(label,query) {
if($scope.panel.mode !== 'query')
return false;
$scope.panel.query.unshift({
query: query,
label: label,
});
$scope.$on('refresh',function(){$scope.get_data()})
$scope.get_data();
}
@ -84,21 +61,40 @@ angular.module('kibana.pie', [])
switch(mode)
{
case 'terms':
$scope.panel.query = {query:"*",field:"_all"};
$scope.panel.query = {field:"_all"};
break;
case 'goal':
$scope.panel.query = {query:"*",goal:100};
$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(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
$scope.panel.loading = true;
var request = $scope.ejs.Request().indices($scope.index);
var request = $scope.ejs.Request().indices(dashboard.indices);
// This could probably be changed to a BoolFilter
var boolQuery = ejs.BoolQuery();
_.each(query.list,function(q) {
boolQuery = boolQuery.should(ejs.QueryStringQuery(q.query || '*'))
})
// Terms mode
if ($scope.panel.mode == "terms") {
@ -109,10 +105,8 @@ angular.module('kibana.pie', [])
.exclude($scope.panel.exclude)
.facetFilter(ejs.QueryFilter(
ejs.FilteredQuery(
ejs.QueryStringQuery($scope.panel.query.query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
boolQuery,
filterSrv.getBoolFilter(filterSrv.ids)
)))).size(0)
$scope.populate_modal(request);
@ -141,11 +135,8 @@ angular.module('kibana.pie', [])
// Goal mode
} else {
request = request
.query(ejs.QueryStringQuery($scope.panel.query.query || '*'))
.filter(ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
.cache(false))
.query(boolQuery)
.filter(filterSrv.getBoolFilter(filterSrv.ids))
.size(0)
$scope.populate_modal(request);
@ -169,26 +160,14 @@ angular.module('kibana.pie', [])
$scope.modal = {
title: "Inspector",
body : "<h5>Last Elasticsearch Query</h5><pre>"+
'curl -XGET '+config.elasticsearch+'/'+$scope.index+"/_search?pretty -d'\n"+
'curl -XGET '+config.elasticsearch+'/'+dashboard.indices+"/_search?pretty -d'\n"+
angular.toJson(JSON.parse(request.toString()),true)+
"'</pre>",
}
}
$scope.build_search = function(field,value) {
$scope.panel.query.query = add_to_query($scope.panel.query.query,field,value,false)
$scope.get_data();
eventBus.broadcast($scope.$id,$scope.panel.group,'query',[$scope.panel.query.query]);
}
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
})
.directive('pie', function() {
.directive('pie', function(query, filterSrv, dashboard) {
return {
restrict: 'A',
link: function(scope, elem, attrs) {
@ -228,8 +207,8 @@ angular.module('kibana.pie', [])
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>';
return '<div "style="font-size:8pt;text-align:center;padding:2px;color:white;">'+
series.info.alias+'<br/>'+Math.round(series.percent)+'%</div>';
},
threshold: 0.1
}
@ -258,7 +237,7 @@ angular.module('kibana.pie', [])
clickable: true
},
legend: { show: false },
colors: ['#86B22D','#BF6730','#1D7373','#BFB930','#BF3030','#77207D']
colors: query.colors
};
// Populate element
@ -269,7 +248,7 @@ angular.module('kibana.pie', [])
}
}
function piett(x, y, contents) {
function tt(x, y, contents) {
var tooltip = $('#pie-tooltip').length ?
$('#pie-tooltip') : $('<div id="pie-tooltip"></div>');
@ -287,16 +266,19 @@ angular.module('kibana.pie', [])
}
elem.bind("plotclick", function (event, pos, object) {
if (!object)
if (!object) {
return;
if(scope.panel.mode === 'terms')
scope.build_search(scope.panel.query.field,object.series.label);
}
if(scope.panel.mode === 'terms') {
filterSrv.set({type:'terms',field:scope.panel.query.field,value:object.series.label})
dashboard.refresh();
}
});
elem.bind("plothover", function (event, pos, item) {
if (item) {
var percent = parseFloat(item.series.percent).toFixed(1) + "%";
piett(pos.pageX, pos.pageY, "<div style='vertical-align:middle;display:inline-block;background:"+item.series.color+";height:15px;width:15px;border-radius:10px;'></div> " +
tt(pos.pageX, pos.pageY, "<div style='vertical-align:middle;display:inline-block;background:"+item.series.color+";height:15px;width:15px;border-radius:10px;'></div> " +
(item.series.label||"")+ " " + percent);
} else {
$("#pie-tooltip").remove();

@ -1,5 +1,15 @@
<style>
</style>
<a class="close" ng-click="render();dismiss();" href="">×</a>
<input class="input-medium" type="text" ng-model="queries.list[id].alias" placeholder='Alias...' />
<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>
<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>

@ -39,12 +39,10 @@ angular.module('kibana.query', [])
}
$scope.refresh = function(query) {
console.log('refresh')
$rootScope.$broadcast('refresh')
}
$scope.render = function(query) {
console.log('render')
$rootScope.$broadcast('render')
}

@ -29,7 +29,7 @@
*/
angular.module('kibana.table', [])
.controller('table', function($rootScope, $scope, eventBus, fields, query) {
.controller('table', function($rootScope, $scope, eventBus, fields, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -61,10 +61,6 @@ angular.module('kibana.table', [])
$scope.set_listeners = function(group) {
$scope.$on('refresh',function(){$scope.get_data()})
eventBus.register($scope,'time',function(event,time) {
$scope.panel.offset = 0;
set_time(time)
});
eventBus.register($scope,'sort', function(event,sort){
$scope.panel.sort = _.clone(sort);
$scope.get_data();
@ -112,26 +108,26 @@ angular.module('kibana.table', [])
}
$scope.build_search = function(field,value,negate) {
_.each(query.list,function(q) {
q.query = add_to_query(q.query,field,value,negate);
})
var query = (negate ? '-':'+')+field+":\""+value+"\""
filterSrv.set({type:'querystring',query:query})
$scope.panel.offset = 0;
$rootScope.$broadcast('refresh')
dashboard.refresh();
}
$scope.get_data = function(segment,query_id) {
$scope.panel.error = false;
// Make sure we have everything for the request to complete
if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
}
$scope.panel.loading = true;
var _segment = _.isUndefined(segment) ? 0 : segment
$scope.segment = _segment;
var request = $scope.ejs.Request().indices($scope.index[_segment])
var request = $scope.ejs.Request().indices(dashboard.indices[_segment])
var boolQuery = ejs.BoolQuery();
_.each(query.list,function(q) {
@ -141,11 +137,8 @@ angular.module('kibana.table', [])
request = request.query(
ejs.FilteredQuery(
boolQuery,
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to)
)
)
filterSrv.getBoolFilter(filterSrv.ids)
))
.highlight(
ejs.Highlight($scope.panel.highlight)
.fragmentSize(2147483647) // Max size of a 32bit unsigned int
@ -209,10 +202,10 @@ angular.module('kibana.table', [])
// 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 ||
!(($scope.panel.sort[0] === $scope.time.field) && $scope.panel.sort[1] === 'desc')) &&
_segment+1 < $scope.index.length
if($scope.data.length < $scope.panel.size*$scope.panel.pages
//($scope.data.length < $scope.panel.size*$scope.panel.pages
// || !(($scope.panel.sort[0] === $scope.time.field) && $scope.panel.sort[1] === 'desc'))
&& _segment+1 < dashboard.indices.length
) {
$scope.get_data(_segment+1,$scope.query_id)
}
@ -265,12 +258,6 @@ angular.module('kibana.table', [])
}
function set_time(time) {
$scope.time = time;
$scope.index = _.isUndefined(time.index) ? $scope.index : time.index
$scope.get_data();
}
})
.filter('highlight', function() {
return function(text) {

@ -8,40 +8,6 @@
<input type="text" class="input-small" ng-model="panel.timefield">
</div>
</div>
<div class="row-fluid">
<h5>Index Settings</h5>
<div ng-show="panel.index_interval != 'none'" class="row-fluid">
<div class="span12">
<p class="small">
Time stamped indices use your selected time range to create a list of
indices that match a specified timestamp pattern. This can be very
efficient for some data sets (eg, logs) For example, to match the
default logstash index pattern you might use
<code>[logstash-]YYYY.MM.DD</code>. The [] in "[logstash-]" are
important as they instruct Kibana not to treat those letters as a
pattern.
</p>
<p class="small">
See <a href="http://momentjs.com/docs/#/displaying/format/">http://momentjs.com/docs/#/displaying/format/</a>
for documentation on date formatting.
</p>
</div>
</div>
<div class="row-fluid">
<div class="span2">
<h6>Timestamp</h6><select class="input-mini" ng-model="panel.index_interval" ng-options='f for f in ["none","hour","day","week","month","year"]'></select>
</div>
<div class="span5">
<h6>Index <span ng-show="panel.index_interval != 'none'">pattern <small>Absolutes in []</small></span></h6>
<input type="text" class="input-medium" ng-model="panel.index">
</div>
<div class="span4">
<h6>Failover Index <small>If index not found</small></h6>
<input type="text" class="input-medium" ng-model="panel.defaultindex">
</div>
</div>
</div>
<div class="row-fluid">
<h5>Relative mode <small>settings</small></h5>
<div class="span6">

@ -11,9 +11,6 @@
* 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.
* index :: Index pattern to match. Literals should be double quoted. Default: '_all'
* defaultindex :: Index to failover to if index not found
* index_interval :: Time between timestamped indices (can be 'none') for static index
* refresh: Object containing refresh parameters
* enable :: true/false, enable auto refresh by default. Default: false
* interval :: Seconds between auto refresh. Default: 30
@ -28,7 +25,7 @@
*/
angular.module('kibana.timepicker', [])
.controller('timepicker', function($scope, eventBus, $timeout, timer, $http, kbnIndex) {
.controller('timepicker', function($scope, $rootScope, eventBus, $timeout, timer, $http, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -37,9 +34,6 @@ angular.module('kibana.timepicker', [])
time_options : ['5m','15m','1h','6h','12h','24h','2d','7d','30d'],
timespan : '15m',
timefield : '@timestamp',
index : '_all',
defaultindex : "_all",
index_interval: "none",
timeformat : "",
group : "default",
refresh : {
@ -58,6 +52,7 @@ angular.module('kibana.timepicker', [])
// unnecessary refreshes during changes
$scope.refresh_interval = $scope.panel.refresh.interval
// Init a private time object with Date() objects depending on mode
switch($scope.panel.mode) {
case 'absolute':
@ -86,35 +81,20 @@ angular.module('kibana.timepicker', [])
if ($scope.panel.refresh.enable)
$scope.set_interval($scope.panel.refresh.interval);
// In the case that a panel is not ready to receive a time event, it may
// request one be sent by broadcasting a 'get_time' with its _id to its group
// This panel can handle multiple groups
eventBus.register($scope,"get_time", function(event,id) {
eventBus.broadcast($scope.$id,id,'time',compile_time($scope.time))
});
// In case some other panel broadcasts a time, set us to an absolute range
eventBus.register($scope,"set_time", function(event,time) {
$scope.panel.mode = 'absolute';
set_timepicker(moment(time.from),moment(time.to))
$scope.time_apply()
});
eventBus.register($scope,"zoom", function(event,factor) {
var _timespan = ($scope.time.to.valueOf() - $scope.time.from.valueOf());
try {
if($scope.panel.mode != 'absolute') {
$scope.panel.mode = 'since'
set_timepicker(moment($scope.time.to.valueOf() - _timespan*factor),$scope.time.to)
} else {
var _center = $scope.time.to.valueOf() - _timespan/2
set_timepicker(moment(_center - (_timespan*factor)/2),
moment(_center + (_timespan*factor)/2))
}
} catch (e) {
console.log(e)
}
$scope.time_apply();
$scope.$on('refresh', function() {
var time = filterSrv.timeRange('min')
if($scope.time.from.diff(moment.utc(time.from)) != 0
|| $scope.time.to.diff(moment.utc(time.to)) != 0)
{
$scope.panel.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()
}
});
}
@ -146,10 +126,26 @@ angular.module('kibana.timepicker', [])
}
}
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() {
@ -201,45 +197,36 @@ angular.module('kibana.timepicker', [])
};
}
$scope.time_apply = function() {
$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()
// Get indices for the time period, then broadcast time range and index list
// in a single object. Not sure if I like this.
if($scope.panel.index_interval !== 'none') {
kbnIndex.indices($scope.time.from,
$scope.time.to,
$scope.panel.index,
$scope.panel.index_interval
).then(function (p) {
if(p.length > 0) {
$scope.time.index = p;
eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time))
} else {
$scope.panel.error = "Could not match index pattern to any ElasticSearch indices"
}
});
} else {
$scope.time.index = [$scope.panel.index];
eventBus.broadcast($scope.$id,$scope.panel.group,'time',compile_time($scope.time))
}
set_time_filter($scope.time)
dashboard.refresh();
// 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"),
index : $scope.time.index,
};
};
function set_time_filter(time) {
time.type = 'time'
// Check if there's a time filter we remember, if not, set one and remember it
if(!_.isUndefined($scope.panel.filter_id) &&
!_.isUndefined(filterSrv.list[$scope.panel.filter_id]) &&
filterSrv.list[$scope.panel.filter_id].type == 'time')
{
filterSrv.set(compile_time(time),$scope.panel.filter_id)
} else {
delete $scope.panel.time;
$scope.panel.filter_id = filterSrv.set(compile_time(time))
}
};
return $scope.panel.filter_id;
}
// Prefer to pass around Date() objects in the EventBus since interacting with
// moment objects in libraries that are expecting Date()s can be tricky
@ -247,8 +234,6 @@ angular.module('kibana.timepicker', [])
time = _.clone(time)
time.from = time.from.toDate()
time.to = time.to.toDate()
time.interval = $scope.panel.index_interval
time.pattern = $scope.panel.index
return time;
}

@ -5,7 +5,7 @@
<span ng-class="{'text-success': query.hits.new >= query.hits.old, 'text-error': query.hits.old > query.hits.new}" class='strong'>
<i class='large' ng-class="{'icon-caret-up': query.hits.new >= query.hits.old, 'icon-caret-down': query.hits.old > query.hits.new}"></i> {{query.percent}}%
</span>
<span class="tiny pointer light" bs-tooltip="'Then: '+query.hits.old+', Now: '+query.hits.new" ng-show="query.label != ''">({{query.info.alias}})</span>
<span class="tiny pointer light" bs-tooltip="'Then: '+query.hits.old+', Now: '+query.hits.new" ng-show="query.info.alias != ''">({{query.info.alias}})</span>
<br ng-show="panel.arrangement == 'vertical'">
</div>
</kibana-panel>

@ -19,7 +19,7 @@
*/
angular.module('kibana.trends', [])
.controller('trends', function($scope, eventBus, kbnIndex, query) {
.controller('trends', function($scope, eventBus, kbnIndex, query, dashboard, filterSrv) {
// Set and populate defaults
var _d = {
@ -34,15 +34,12 @@ angular.module('kibana.trends', [])
$scope.init = function () {
$scope.hits = 0;
$scope.$on('refresh',function(){$scope.get_data()})
eventBus.register($scope,'time', function(event,time){
set_time(time)
});
eventBus.register($scope,'query', function(event, query) {
$scope.panel.query = _.map(query,function(q) {
return {query: q, label: q};
})
$scope.get_data();
});
// Now that we're all setup, request the time from our group
eventBus.broadcast($scope.$id,$scope.panel.group,'get_time')
}
@ -52,8 +49,11 @@ angular.module('kibana.trends', [])
$scope.panel.loading = true;
// Make sure we have everything for the request to complete
if(_.isUndefined($scope.index) || _.isUndefined($scope.time))
if(dashboard.indices.length == 0) {
return
} else {
$scope.index = dashboard.indices
}
$scope.old_time = {
from : new Date($scope.time.from.getTime() - interval_to_seconds($scope.panel.ago)*1000),
@ -67,9 +67,8 @@ angular.module('kibana.trends', [])
_.each(query.ids, function(id) {
var q = $scope.ejs.FilteredQuery(
ejs.QueryStringQuery(query.list[id].query || '*'),
ejs.RangeFilter($scope.time.field)
.from($scope.time.from)
.to($scope.time.to))
filterSrv.getBoolFilter(filterSrv.ids))
request = request
.facet($scope.ejs.QueryFacet(id)
.query(q)
@ -110,7 +109,6 @@ angular.module('kibana.trends', [])
// Populate scope when we have results
function process_results(results) {
results.then(function(results) {
console.log(results)
$scope.panel.loading = false;
if(_segment == 0) {
@ -124,9 +122,17 @@ angular.module('kibana.trends', [])
$scope.panel.error = $scope.parse_error(results.error);
return;
}
if($scope.query_id === query_id) {
// Convert facet ids to numbers
var facetIds = _.map(_.keys(results.facets),function(k){if(!isNaN(k)){return parseInt(k)}})
// Make sure we're still on the same query/queries
if($scope.query_id === query_id &&
_.intersection(facetIds,query.ids).length == query.ids.length
) {
var i = 0;
_.each(query.ids, function(id) {
var v = results.facets[id]
var n = results.facets[id].count
var o = results.facets['old_'+id].count
@ -193,7 +199,6 @@ angular.module('kibana.trends', [])
function set_time(time) {
$scope.time = time;
$scope.index = time.index || $scope.index
$scope.get_data();
}

@ -12,9 +12,64 @@
<label class="small"> Editable </label><input type="checkbox" ng-model="dashboard.current.editable" ng-checked="dashboard.current.editable" />
</div>
</div>
<div class="row-fluid">
<h4>Index Settings</h4>
<div ng-show="dashboard.current.index.interval != 'none'" class="row-fluid">
<div class="span12">
<p class="small">
Time stamped indices use your selected time range to create a list of
indices that match a specified timestamp pattern. This can be very
efficient for some data sets (eg, logs) For example, to match the
default logstash index pattern you might use
<code>[logstash-]YYYY.MM.DD</code>. The [] in "[logstash-]" are
important as they instruct Kibana not to treat those letters as a
pattern.
</p>
<p class="small">
See <a href="http://momentjs.com/docs/#/displaying/format/">http://momentjs.com/docs/#/displaying/format/</a>
for documentation on date formatting.
</p>
</div>
</div>
<div class="row-fluid">
<div class="span3">
<h6>Timestamping</h6><select class="input-small" ng-model="dashboard.current.index.interval" ng-options='f for f in ["none","hour","day","week","month","year"]'></select>
</div>
<div class="span5">
<h6>Index <span ng-show="dashboard.current.index.interval != 'none'">pattern <small>Absolutes in []</small></span></h6>
<input type="text" class="input-medium" ng-model="dashboard.current.index.pattern">
</div>
<div class="span4">
<h6>Failover Index <small>If index not found</small></h6>
<input type="text" class="input-medium" ng-model="dashboard.current.index.default">
</div>
</div>
</div>
<hr/>
<h4>Rows</h4>
<div class="row-fluid">
<form>
<div class="span5">
<label class="small">Title</label>
<input type="text" class="input-large" ng-model='row.title'></input>
</div>
<div class="span2">
<label class="small">Height</label>
<input type="text" class="input-mini" ng-model='row.height'></input>
</div>
<div class="span1">
<label class="small"> Editable </label>
<input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
</div>
<div class="span4">
<label>&nbsp</label>
<button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-primary">Create Row</button>
</div>
</form>
</div>
<div class="row-fluid">
<div class="span12">
<h4>Rows</h4>
<table class="table table-condensed table-striped">
<thead>
<th>Title</th>
@ -30,23 +85,6 @@
</table>
</div>
</div>
<h4>New row</h4>
<div class="row-fluid">
<div class="span8">
<label class="small">Title</label>
<input type="text" class="input-large" ng-model='row.title'></input>
</div>
<div class="span2">
<label class="small">Height</label>
<input type="text" class="input-mini" ng-model='row.height'></input>
</div>
<div class="span1">
<label class="small"> Editable </label>
<input type="checkbox" ng-model="row.editable" ng-checked="row.editable" />
</div>
</div>
<button ng-click="add_row(dashboard.current,row); reset_row();" class="btn btn-primary">Create Row</button><br>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" ng-click="dismiss();reset_panel();">Close</button>

Loading…
Cancel
Save