Commit 125ff957 by Torkel Ödegaard

Merge branch 'influxdb' Issue #103, influxdb support ready for testing and feedback

parents 6d59d217 73dcd9e1
......@@ -52,14 +52,17 @@ function (_, crypto) {
if (options.graphiteUrl) {
settings.datasources = {
graphite: {
name: 'default',
type: 'graphite',
url: options.graphiteUrl,
default: true
}
};
}
_.map(settings.datasources, parseBasicAuth);
_.each(settings.datasources, function(datasource, key) {
datasource.name = key;
parseBasicAuth(datasource);
});
var elasticParsed = parseBasicAuth({ url: settings.elasticsearch });
settings.elasticsearchBasicAuth = elasticParsed.basicAuth;
......
......@@ -8,4 +8,5 @@ define([
'./metricKeys',
'./graphiteTarget',
'./graphiteImport',
'./influxTargetCtrl',
], function () {});
\ No newline at end of file
define([
'angular'
],
function (angular) {
'use strict';
var module = angular.module('kibana.controllers');
module.controller('InfluxTargetCtrl', function($scope) {
$scope.init = function() {
if (!$scope.target.function) {
$scope.target.function = 'mean';
}
};
$scope.duplicate = function() {
var clone = angular.copy($scope.target);
$scope.panel.targets.push(clone);
};
});
});
\ No newline at end of file
......@@ -63,6 +63,27 @@ function (angular, app, _) {
}
};
$scope.duplicatePanel = function(panel, row) {
row = row || $scope.row;
var currentRowSpan = $scope.rowSpan(row);
if (currentRowSpan <= 9) {
row.panels.push(angular.copy(panel));
}
else {
var rowsList = $scope.dashboard.current.rows;
var rowIndex = _.indexOf(rowsList, row);
if (rowIndex === rowsList.length - 1) {
var newRow = angular.copy($scope.row);
newRow.panels = [];
$scope.dashboard.current.rows.push(newRow);
$scope.duplicatePanel(panel, newRow);
}
else {
$scope.duplicatePanel(panel, rowsList[rowIndex+1]);
}
}
};
/** @scratch /panels/0
* [[panels]]
* = Panels
......@@ -111,7 +132,6 @@ function (angular, app, _) {
$scope.init();
}
);
});
});
\ No newline at end of file
......@@ -69,8 +69,7 @@
"nullPointMode": "connected",
"steppedLine": false,
"tooltip": {
"value_type": "cumulative",
"query_as_alias": true
"value_type": "cumulative"
},
"targets": [
{
......
......@@ -32,5 +32,4 @@ function (angular, app, _) {
}
};
});
});
\ No newline at end of file
......@@ -82,4 +82,52 @@ function (angular, $) {
}
};
});
angular
.module('kibana.directives')
.directive('gfDropdown', function ($parse, $compile, $timeout) {
function buildTemplate(items, ul) {
if (!ul) {
ul = [
'<ul class="dropdown-menu" role="menu" aria-labelledby="drop1">',
'</ul>'
];
}
angular.forEach(items, function (item, index) {
if (item.divider) {
return ul.splice(index + 1, 0, '<li class="divider"></li>');
}
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? '" ng-click="' + item.click + '"' : '') +
(item.target ? '" target="' + item.target + '"' : '') + (item.method ? '" data-method="' + item.method + '"' : '') +
(item.configModal ? ' config-modal="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {
li += buildTemplate(item.submenu).join('\n');
}
li += '</li>';
ul.splice(index + 1, 0, li);
});
return ul;
}
return {
restrict: 'EA',
scope: true,
link: function postLink(scope, iElement, iAttrs) {
var getter = $parse(iAttrs.gfDropdown), items = getter(scope);
$timeout(function () {
var dropdown = angular.element(buildTemplate(items).join(''));
dropdown.insertAfter(iElement);
$compile(iElement.next('ul.dropdown-menu'))(scope);
});
iElement.addClass('dropdown-toggle').attr('data-toggle', 'dropdown');
}
};
});
});
\ No newline at end of file
......@@ -75,9 +75,6 @@ function (angular, $, kbn, moment, _) {
}
});
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.interval);
var stack = panel.stack ? true : null;
// Populate element
......@@ -96,7 +93,7 @@ function (angular, $, kbn, moment, _) {
bars: {
show: panel.bars,
fill: 1,
barWidth: barwidth/1.5,
barWidth: 1,
zero: false,
lineWidth: 0
},
......@@ -128,6 +125,10 @@ function (angular, $, kbn, moment, _) {
data[i].data = _d;
}
if (panel.bars && data.length && data[0].info.timeStep) {
options.series.bars.barWidth = data[0].info.timeStep / 1.5;
}
addTimeAxis(options);
addGridThresholds(options, panel);
addAnnotations(options);
......@@ -289,7 +290,7 @@ function (angular, $, kbn, moment, _) {
seriesInfo = item.series.info;
format = scope.panel.y_formats[seriesInfo.yaxis - 1];
if (seriesInfo.alias || scope.panel.tooltip.query_as_alias) {
if (seriesInfo.alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
(seriesInfo.alias || seriesInfo.query)+
......
define([
'angular',
'jquery'
'jquery',
'underscore'
],
function (angular, $) {
function (angular, $, _) {
'use strict';
angular
.module('kibana.directives')
.directive('kibanaPanel', function($compile) {
.directive('kibanaPanel', function($compile, $timeout, $rootScope) {
var container = '<div class="panel-container"></div>';
var content = '<div class="panel-content"></div>';
......@@ -28,8 +29,8 @@ function (angular, $) {
'<i class="icon-spinner icon-spin icon-large"></i>' +
'</span>' +
'<span ng-if="panelMeta.menuItems" class="dropdown">' +
'<span class="panel-text panel-title pointer" bs-dropdown="panelMeta.menuItems" tabindex="1" ' +
'<span class="dropdown">' +
'<span class="panel-text panel-title pointer" gf-dropdown="panelMeta.menu" tabindex="1" ' +
'data-drag=true data-jqyoui-options="kbnJqUiDraggableOptions"'+
' jqyoui-draggable="'+
'{'+
......@@ -44,11 +45,6 @@ function (angular, $) {
'</span>' +
'</span>'+
'<span ng-if="!panelMeta.menuItems" config-modal="./app/partials/paneleditor.html" ' +
' class="panel-text panel-title pointer" >' +
'{{panel.title}}' +
'</span>'+
'</div>'+
'</div>\n'+
'</div>';
......@@ -88,8 +84,7 @@ function (angular, $) {
var nameAsPath = name.replace(".", "/");
$scope.require([
'jquery',
'text!panels/'+nameAsPath+'/module.html',
'text!panels/'+nameAsPath+'/editor.html'
'text!panels/'+nameAsPath+'/module.html'
], function ($, moduleTemplate) {
var $module = $(moduleTemplate);
// top level controllers
......@@ -101,9 +96,7 @@ function (angular, $) {
$controllers.first().prepend(panelHeader);
$controllers.first().find('.panel-header').nextAll().wrapAll(content);
$scope.require([
'panels/'+nameAsPath+'/module'
], function() {
$scope.require(['panels/' + nameAsPath + '/module'], function() {
loadModule($module);
});
} else {
......@@ -111,6 +104,126 @@ function (angular, $) {
}
});
});
/*
/* Panel base functionality
/* */
newScope.initPanel = function(scope) {
scope.updateColumnSpan = function(span) {
scope.panel.span = span;
$timeout(function() {
scope.$emit('render');
});
};
function enterFullscreenMode(options) {
var docHeight = $(window).height();
var editHeight = Math.floor(docHeight * 0.3);
var fullscreenHeight = Math.floor(docHeight * 0.7);
var oldTimeRange = scope.range;
scope.height = options.edit ? editHeight : fullscreenHeight;
scope.editMode = options.edit;
if (!scope.fullscreen) {
var closeEditMode = $rootScope.$on('panel-fullscreen-exit', function() {
scope.editMode = false;
scope.fullscreen = false;
delete scope.height;
closeEditMode();
$timeout(function() {
if (oldTimeRange !== $scope.range) {
scope.dashboard.refresh();
}
else {
scope.$emit('render');
}
});
});
}
$(window).scrollTop(0);
scope.fullscreen = true;
$rootScope.$emit('panel-fullscreen-enter');
$timeout(function() {
scope.$emit('render');
});
}
scope.toggleFullscreenEdit = function() {
if (scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
enterFullscreenMode({edit: true});
};
$scope.toggleFullscreen = function() {
if (scope.fullscreen && !scope.editMode) {
$rootScope.$emit('panel-fullscreen-exit');
return;
}
enterFullscreenMode({ edit: false });
};
var menu = [
{
text: 'Edit',
configModal: "app/partials/paneleditor.html",
condition: !scope.panelMeta.fullscreenEdit
},
{
text: 'Edit',
click: "toggleFullscreenEdit()",
condition: scope.panelMeta.fullscreenEdit
},
{
text: "Fullscreen",
click: 'toggleFullscreen()',
condition: scope.panelMeta.fullscreenView
},
{
text: 'Duplicate',
click: 'duplicatePanel(panel)',
condition: true
},
{
text: 'Span',
submenu: [
{ text: '1', click: 'updateColumnSpan(1)' },
{ text: '2', click: 'updateColumnSpan(2)' },
{ text: '3', click: 'updateColumnSpan(3)' },
{ text: '4', click: 'updateColumnSpan(4)' },
{ text: '5', click: 'updateColumnSpan(5)' },
{ text: '6', click: 'updateColumnSpan(6)' },
{ text: '7', click: 'updateColumnSpan(7)' },
{ text: '8', click: 'updateColumnSpan(8)' },
{ text: '9', click: 'updateColumnSpan(9)' },
{ text: '10', click: 'updateColumnSpan(10)' },
{ text: '11', click: 'updateColumnSpan(11)' },
{ text: '12', click: 'updateColumnSpan(12)' },
],
condition: true
},
{
text: 'Remove',
click: 'remove_panel_from_row(row, panel)',
condition: true
}
];
scope.panelMeta.menu = _.where(menu, { condition: true });
};
}
};
});
......
......@@ -2,13 +2,6 @@
ng-init="init()"
style="min-height:{{panel.height || row.height}}"
ng-class="{'panel-fullscreen': fullscreen}">
<div>
<span ng-show='panel.options'>
<a class="link underline small" ng-show='panel.options' ng-click="options=!options">
<i ng-show="!options" class="icon-caret-right"></i><i ng-show="options" class="icon-caret-down"></i> View
</a> |&nbsp
</span>
</div>
<div style="position: relative">
......@@ -32,22 +25,8 @@
</div>
</div>
<div class="tab-content" ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></div>
<div class="editor-row" ng-show="datasources.length > 0">
<div class="section">
<div class="editor-option">
<label class="small">Datasource</label>
<select class="input-large" ng-options="obj.value as obj.name for obj in datasources" ng-model="panel.datasource" ng-change="datasourceChanged()"></select>
</div>
</div>
</div>
</div>
<div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-show="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
</div>
\ No newline at end of file
......@@ -50,7 +50,12 @@ function (_, kbn) {
result.push([currentTime * 1000, currentValue]);
}, this);
if (result.length > 2) {
this.info.timeStep = result[1][0] - result[0][0];
}
if (result.length) {
this.info.avg = (this.info.total / result.length);
this.info.current = result[result.length-1][1];
......
......@@ -24,7 +24,6 @@ function (angular, app, _, require) {
module.controller('text', function($scope) {
$scope.panelMeta = {
status : "Stable",
description : "A static text panel that can use plain text, markdown, or (sanitized) HTML"
};
......@@ -45,6 +44,7 @@ function (angular, app, _, require) {
_.defaults($scope.panel,_d);
$scope.init = function() {
$scope.initPanel($scope);
$scope.ready = false;
};
......
<div class="editor-row">
<div class="editor-row" style="margin-top: 10px;">
<div ng-repeat="target in panel.targets"
class="grafana-target"
......@@ -52,7 +53,7 @@
</ul>
<input type="text"
class="grafana-target-text-input"
class="grafana-target-text-input span12"
ng-model="target.target"
focus-me="showTextEditor"
spellcheck='false'
......@@ -75,7 +76,7 @@
</ul>
</li>
<li ng-repeat="func in functions">
<a class="grafana-target-segment grafana-target-function dropdown-toggle" bs-popover="'app/panels/graphite/funcEditor.html'" data-placement="bottom">
<a class="grafana-target-segment grafana-target-function dropdown-toggle" bs-popover="'app/partials/graphite/funcEditor.html'" data-placement="bottom">
{{func.text}}
</a>
</li>
......@@ -111,6 +112,3 @@
</div>
</div>
<div class="editor-row" style="margin-top: 20px" ng-show="editor.index == 1">
<button class="btn btn-success pull-right" ng-click="add_target(panel.target)">Add target</button>
</div>
\ No newline at end of file
<div class="editor-row" style="margin-top: 10px;">
<div ng-repeat="target in panel.targets"
class="grafana-target"
ng-class="{'grafana-target-hidden': target.hide}"
ng-controller="InfluxTargetCtrl"
ng-init="init()">
<div class="grafana-target-inner-wrapper">
<div class="grafana-target-inner">
<ul class="grafana-target-controls">
<li class="dropdown">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1"
ng-click="duplicate()">
Duplicate
</a>
</li>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeTarget(target)">
<i class="icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-target-controls-left">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
</ul>
<ul class="grafana-segment-list" role="menu">
<li class="grafana-target-segment">
from series
</li>
<li>
<input type="text"
class="input-medium grafana-target-segment-input"
ng-model="target.series"
spellcheck='false'
placeholder="series name"
ng-model-onblur ng-change="get_data()" >
</li>
<li class="grafana-target-segment">
select
</li>
<li>
<input type="text"
class="input-medium grafana-target-segment-input"
ng-model="target.column"
placeholder="value column"
spellcheck='false'
ng-model-onblur ng-change="get_data()" >
</li>
<li class="grafana-target-segment">
function
</li>
<li>
<select class="input-medium grafana-target-segment-input" ng-change="get_data()" ng-model="target.function" ng-options="f for f in ['mean', 'sum', 'min', 'max', 'median', 'derivative', 'stddev']" ></select>
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text"
class="input-mini grafana-target-segment-input"
ng-model="target.interval"
placeholder="{{interval}}"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
spellcheck='false'
ng-model-onblur ng-change="get_data()" >
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<div ng-include src="datasource.editorSrc"></div>
<div class="editor-row" style="margin-top: 20px">
<button class="btn btn-success pull-right" ng-click="add_target(panel.target)">Add query</button>
<div class="btn-group pull-right" style="margin-right: 10px;">
<button class="btn btn-info dropdown-toggle" data-toggle="dropdown" bs-tooltip="'Datasource'">{{datasource.name}} <span class="caret"></span></button>
<ul class="dropdown-menu" role="menu">
<li ng-repeat="datasource in datasources" role="menuitem">
<a ng-click="setDatasource(datasource.value);">{{datasource.name}}</a>
</li>
</ul>
</div>
</div>
<div ng-include="'app/partials/panelgeneral.html'"></div>
<div ng-include="edit_path(panel.type)"></div>
<div ng-repeat="tab in panelMeta.editorTabs">
<div ng-include="'app/partials/panelgeneral.html'"></div>
<div ng-if="!panelMeta.fullEditorTabs" ng-include="edit_path(panel.type)"></div>
<div ng-repeat="tab in panelMeta.editorTabs">
<h5>{{tab.title}}</h5>
<div ng-include="tab.src"></div>
</div>
\ No newline at end of file
</div>
\ No newline at end of file
<div class="editor-row">
<div class="section">
<strong>{{panelMeta.status}}</strong> // <span ng-bind-html="panelMeta.description"></span>
</div>
</div>
<div class="editor-row">
<div class="editor-row">
<div class="section">
<h5>General options</h5>
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
</div>
......@@ -21,4 +17,4 @@
<input type="checkbox" ng-model="panel.spyable" ng-checked="panel.spyable">
</div>
</div>
</div>
\ No newline at end of file
</div>
\ No newline at end of file
......@@ -7,6 +7,5 @@ define([
'./datasourceSrv',
'./keyboardManager',
'./annotationsSrv',
'./graphite/graphiteDatasource',
],
function () {});
\ No newline at end of file
define([
'angular',
'underscore',
'config'
'config',
'./graphite/graphiteDatasource',
'./influxdb/influxdbDatasource',
],
function (angular, _, config) {
'use strict';
var module = angular.module('kibana.services');
module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource) {
module.service('datasourceSrv', function($q, filterSrv, $http, GraphiteDatasource, InfluxDatasource) {
var defaultDatasource = _.findWhere(_.values(config.datasources), { default: true } );
this.default = new GraphiteDatasource(defaultDatasource);
this.get = function(name) {
if (!name) {
return this.default;
if (!name) { return this.default; }
var ds = config.datasources[name];
if (!ds) {
return null;
}
return new GraphiteDatasource(config.datasources[name]);
switch(ds.type) {
case 'graphite':
return new GraphiteDatasource(ds);
case 'influxdb':
return new InfluxDatasource(ds);
}
};
this.listOptions = function() {
......
......@@ -17,6 +17,8 @@ function (angular, _, $, config, kbn, moment) {
this.type = 'graphite';
this.basicAuth = datasource.basicAuth;
this.url = datasource.url;
this.editorSrc = 'app/partials/graphite/editor.html';
this.name = datasource.name;
}
GraphiteDatasource.prototype.query = function(options) {
......
define([
'angular',
'underscore',
'kbn'
],
function (angular, _, kbn) {
'use strict';
var module = angular.module('kibana.services');
module.factory('InfluxDatasource', function($q, $http) {
function InfluxDatasource(datasource) {
this.type = 'influxDB';
this.editorSrc = 'app/partials/influxDB/editor.html';
this.url = datasource.url;
this.username = datasource.username;
this.password = datasource.password;
this.name = datasource.name;
this.templateSettings = {
interpolate : /\[\[([\s\S]+?)\]\]/g,
};
}
InfluxDatasource.prototype.query = function(options) {
var promises = _.map(options.targets, function(target) {
if (!target.series || !target.column || target.hide) {
return [];
}
var template = "select [[func]]([[column]]) from [[series]] where [[timeFilter]] group by time([[interval]]) order asc";
var templateData = {
series: target.series,
column: target.column,
func: target.function,
timeFilter: getTimeFilter(options),
interval: target.interval || options.interval
};
var query = _.template(template, templateData, this.templateSettings);
console.log(query);
return this.doInfluxRequest(query).then(handleInfluxQueryResponse);
}, this);
return $q.all(promises).then(function(results) {
return { data: _.flatten(results) };
});
};
InfluxDatasource.prototype.doInfluxRequest = function(query) {
var params = {
u: this.username,
p: this.password,
q: query
};
var options = {
method: 'GET',
url: this.url + '/series',
params: params,
};
return $http(options);
};
function handleInfluxQueryResponse(results) {
var output = [];
_.each(results.data, function(series) {
var timeCol = series.columns.indexOf('time');
_.each(series.columns, function(column, index) {
if (column === "time" || column === "sequence_number") {
return;
}
console.log("series:"+series.name + ": "+series.points.length + " points");
var target = series.name + "." + column;
var datapoints = [];
for(var i = 0; i < series.points.length; i++) {
var t = Math.floor(series.points[i][timeCol] / 1000);
var v = series.points[i][index];
datapoints[i] = [v,t];
}
output.push({ target:target, datapoints:datapoints });
});
});
return output;
}
function getTimeFilter(options) {
var from = getInfluxTime(options.range.from);
var until = getInfluxTime(options.range.to);
if (until === 'now()') {
return 'time > now() - ' + from;
}
return 'time > ' + from + ' and time < ' + until;
}
function getInfluxTime(date) {
if (_.isString(date)) {
if (date === 'now') {
return 'now()';
}
else if (date.indexOf('now') >= 0) {
return date.substring(4);
}
date = kbn.parseDate(date);
}
return to_utc_epoch_seconds(date);
}
function to_utc_epoch_seconds(date) {
return (date.getTime() / 1000).toFixed(0) + 's';
}
return InfluxDatasource;
});
});
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -318,15 +318,32 @@
input[type=text].grafana-target-text-input {
padding: 5px 7px;
border: 1px solid @grafanaTargetSegmentBorder;
border: none;
margin: 0px;
background: transparent;
width: 80%;
float: left;
color: @grafanaTargetColor;
border-radius: 0;
}
input[type=text].grafana-target-segment-input {
border: none;
border-right: 1px solid @grafanaTargetSegmentBorder;
margin: 0px;
border-radius: 0;
height: 22px;
line-height: 22px;
}
select.grafana-target-segment-input {
border: none;
border-right: 1px solid @grafanaTargetSegmentBorder;
margin: 0px;
border-radius: 0;
height: 30px;
line-height: 30px;
}
.grafana-target .dropdown {
padding: 0; margin: 0;
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment