Compare commits

..

60 Commits

Author SHA1 Message Date
Torkel Ödegaard
0015f81645 Implementation of legend values (min, max, current, total, avg), Closes #60, Fixes #14 2014-02-06 13:29:55 +01:00
Torkel Ödegaard
ed76335c96 small change to time axis formats, more graphite like now 2014-02-06 10:39:24 +01:00
Torkel Ödegaard
e0a35a3958 work in progress for legend values 2014-02-06 10:19:26 +01:00
Torkel Ödegaard
d9ec9ed1ef removed metrics tab in dash editor modal (only created confusion, the feature is not ready) 2014-02-06 07:55:19 +01:00
Torkel Ödegaard
0032f934c9 Closes #62, there is now a New button in the open / search dashboard view. 2014-02-05 20:56:48 +01:00
Torkel Ödegaard
8816e18027 Merge pull request #63 from jippi/feature/unique-local-storage-name
Make sure kibana and grafana on same hostname do not interfere with each other
2014-02-05 19:16:02 +01:00
Christian Winther
e8be950752 Make sure kibana and grafana on same hostname do not interfere with each other
This happens if you have kibana on /kibana and grafana on /grafana, resulting in some bad redirects
2014-02-05 18:02:15 +00:00
Torkel Ödegaard
aea4b37760 added highestCurrent function (Fixes #57) 2014-02-05 16:10:09 +01:00
Torkel Ödegaard
1aff1a0000 mini improvement to xasis format (now shows just hours for larger time spans) 2014-02-05 15:39:50 +01:00
Torkel Ödegaard
df54c17dca small refactoring of check if other panel is in fullscreen edit/view mode 2014-02-05 15:26:34 +01:00
Torkel Ödegaard
73c5883f0d small fix for png width error when resize in edit mode / fullscreen mode 2014-02-05 15:13:24 +01:00
Torkel Ödegaard
e3561ce555 fixed jshint errors, dam fever is coming back, time to sleep... 2014-02-05 13:35:32 +01:00
Torkel Ödegaard
a5feb639d9 Merge pull request #58 from andreparodi/master
added nonNegativeDerivative function
2014-02-05 13:28:38 +01:00
Torkel Ödegaard
ac33286c8c added sortByName function (thx @jippi) 2014-02-05 13:23:14 +01:00
Torkel Ödegaard
19d5d32229 simplified a lot of the time series data manipulation prior to handing it to flot, kibana used a lot of code to sort and make sure all time points needed had values, this is not needed for graphite time series data. Fixes som strange issues with "null point mode" and inconsistent rendering between graphite and grafana flot, 2014-02-05 13:14:00 +01:00
Torkel Ödegaard
d90f2537a4 added unit (y axis format) mapping support for png renderer (#22) 2014-02-05 11:30:54 +01:00
Torkel Ödegaard
487aa1f2c3 Added hideAxes and hideYAxis mapping support for png renderer. #22 2014-02-05 11:21:37 +01:00
Torkel Ödegaard
be9ab15c4e added grid min max support for png renderer (url property mappings that is), #22 2014-02-05 11:14:13 +01:00
Torkel Ödegaard
7d4f0d24d5 restructured style settings between axes & grid, and Display Styles 2014-02-05 11:12:47 +01:00
Torkel Ödegaard
bafa334b94 moved timezone correction setting from graph style to dashboard 2014-02-05 09:42:49 +01:00
Torkel Ödegaard
80c5f99f3b Removed pointless setting "selectable" (interactive) 2014-02-05 08:56:10 +01:00
Torkel Ödegaard
212bf5df98 small fix to png rendering 2014-02-05 08:41:46 +01:00
Andre Parodi
32189afc67 added nonNegativeDerivative function 2014-02-02 23:16:53 +01:00
Torkel Ödegaard
0fa6b34f1a working on native png renderer support 2014-02-01 22:06:10 +01:00
Torkel Ödegaard
4d6bb8b6be progress on translating grafana/flot options to graphite png renderer options 2014-02-01 21:23:43 +01:00
Torkel Ödegaard
82da75bc0c began work on adding support for native graphite png renderer support (#22) 2014-02-01 21:23:22 +01:00
Torkel Ödegaard
beda378314 Fixes #45, zero values treated as null values 2014-02-01 14:09:04 +01:00
Torkel Ödegaard
6734864acd small change to readme.md 2014-02-01 13:16:39 +01:00
Torkel Ödegaard
9f548870d0 added build status to readme.md 2014-02-01 13:00:32 +01:00
Torkel Ödegaard
d2bae43d26 Adding travis-ci build 2014-02-01 11:26:22 +01:00
Torkel Ödegaard
e77e43faab Fixes #46, you can now edit a graph with no title 2014-02-01 11:02:24 +01:00
Torkel Ödegaard
224d2f92c3 Save now closes edit mode. (#52). 2014-02-01 10:51:35 +01:00
Torkel Ödegaard
250e354659 Dashboard search fix, only query against title field (Closes #51,Fixes #24) 2014-02-01 09:53:57 +01:00
John Dyer
2e59587c8e Clean up, and set default in config to false 2014-02-01 09:53:56 +01:00
John Dyer
6f4520254b Make search support elastic search clusters with _all disabled. Feature is disabled by default 2014-02-01 09:53:55 +01:00
Torkel Ödegaard
b939c02da0 updated property docs in config.js 2014-02-01 09:03:51 +01:00
Torkel Ödegaard
9e900d5885 fixed small variable name issue with pull request #55 (jshint warning) 2014-02-01 08:53:59 +01:00
Torkel Ödegaard
38d3450160 refactoring, moved graph directive to a seperate js file 2014-02-01 08:47:16 +01:00
Torkel Ödegaard
2c5bfec089 Merge pull request #55 from oroce/elasticsearch-basicauth
basic authentication added for elasticsearch
2014-01-31 23:45:48 -08:00
oroce
a56a2b0057 basic authentication added for elasticsearch 2014-01-31 21:23:32 +01:00
Torkel Ödegaard
89224401a9 Fixes #38, Lexer & parser now handles metrics segments that begin with numbers or only have numbers 2014-01-29 11:00:25 +01:00
Torkel Ödegaard
1c17c8661a Fixes #43, add grunt-cli as dev dependency 2014-01-29 09:17:17 +01:00
Torkel Ödegaard
4ed65891c2 Merge pull request #39 from jeviolle/master
fixed missing module for 'moment' and jslint missing semi colon
2014-01-28 03:37:35 -08:00
Rick Briganti
49e131ce21 updated how moment is included and removed moment as a dep since it is included 2014-01-27 17:30:28 -05:00
Rick Briganti
06f4b017d6 fixed missing module for 'moment' and jslint missing semi colon 2014-01-27 14:01:36 -05:00
Torkel Ödegaard
f52450aef4 Merge pull request #37 from tmeinlschmidt/master
fixes 'derivate' function name - correct name is 'derivative'
2014-01-26 23:33:43 -08:00
Tom Meinlschmidt
6476843e95 - fixes 'derivate' function name - corrent name is 'derivative' 2014-01-27 02:00:04 +01:00
Torkel Ödegaard
67d11dffb9 small readme fix 2014-01-25 17:25:21 +01:00
Torkel Ödegaard
58dbb01e76 Added new config setting timezoneOffset, usefull when your graphite server is on a different timezone thant your users browsers. This is not optimal (for example if your users have different timezones) can be improved by finding a way to query the timezone different between browser and graphite server. Or fix so that the &tz graphite parameter works for json, then all absolute filters can use UTC time. #31 2014-01-24 17:00:54 +01:00
Torkel Ödegaard
81e9a483bc Merge pull request #29 from rsommer/patch-1
Update graphiteSrv.js
2014-01-24 05:23:04 -08:00
Torkel Ödegaard
5688f792cb Merge pull request #30 from johanwiren/fix_graphite_import
Fixed graphite import to include all graphs in a dashboard.
2014-01-24 05:16:54 -08:00
Johan Wirén
3ffc04be4d Fixed graphite import to include all graphs in a dashboard. 2014-01-24 14:08:09 +01:00
Roland Sommer
55e586c2c6 Update graphiteSrv.js
If 'now' is not in 'data', the result is -1, so it should be '>= 0' ...
2014-01-24 13:32:16 +01:00
Torkel Ödegaard
76b535a2e4 Merge pull request #26 from johanwiren/add_title_on_import
Sets graph title when importing from graphite
2014-01-23 20:27:34 -08:00
Johan Wirén
6e27f97bc9 Sets graph title when importing from graphite 2014-01-24 00:07:43 +01:00
Torkel Ödegaard
d9ada8d94e Merge pull request #23 from longnguyen11288/master
Fixed typo in import
2014-01-23 10:39:24 -08:00
Long Nguyen
b3d67c3ed4 Fixed typo in import 2014-01-23 10:35:56 -08:00
Torkel Odegaard
459b0dc5bd Updated readme with link to getting started guide 2014-01-23 19:19:56 +01:00
Torkel Ödegaard
aefd95890e Merge pull request #21 from dcarley/probject_typo
Correct typo "probject".
2014-01-23 10:06:50 -08:00
Dan Carley
36d66494d3 Correct typo "probject". 2014-01-23 17:48:51 +00:00
37 changed files with 1835 additions and 1001 deletions

5
.travis.yml Normal file
View File

@@ -0,0 +1,5 @@
language: node_js
node_js:
- "0.10"
before_script:
- npm install -g grunt-cli

View File

@@ -1,7 +1,9 @@
# Grafana - Graphite Dashboard
[Grafana](http://grafana.org) [![Build Status](https://api.travis-ci.org/torkelo/grafana.png)](https://travis-ci.org/torkelo/grafana)
=================
A beautiful, easy to use and feature rich Graphite dashboard replacement and graph editor. Visit [grafana.org](http://grafana.org) for screenshots and an overview.
![](http://grafana.org/assets/img/edit_dashboards.png)
# Features
## Graphite Target Editor
- Graphite target expression parser
@@ -45,8 +47,11 @@ To run from master:
- Clone this repository
- Start a web server in src folder
- Or create a optimized & minified build:
-- npm install (requires nodejs)
-- grunt build
- npm install (requires nodejs)
- grunt build
When you have Grafana up an running, read the [Getting started](https://github.com/torkelo/grafana/wiki/Getting-started) guide for
an introduction on how to use Grafana.
# Graphite server config
If you haven't used an alternative dashboard for graphite before you need to enable cross-domain origin request. For Apache 2.x:

View File

@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
"version": "1.0.3",
"version": "1.0.4",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
@@ -43,7 +43,15 @@
"karma": "~0.10.9",
"grunt-karma": "~0.6.2",
"karma-mocha": "~0.1.1",
"karma-expect": "~1.0.0"
"karma-expect": "~1.0.0",
"grunt-cli": "~0.1.13"
},
"engines": {
"node": "0.10.x",
"npm": "1.2.x"
},
"scripts": {
"test": "grunt"
},
"license": "Apache License"
}

View File

@@ -13,11 +13,13 @@ function (_, crypto) {
* @type {Object}
*/
var defaults = {
elasticsearch : "http://"+window.location.hostname+":9200",
graphiteUrl : "http://"+window.location.hostname+":8080",
panel_names : [],
default_route : '/dashboard/file/default.json',
grafana_index : 'grafana-dash'
elasticsearch : "http://"+window.location.hostname+":9200",
graphiteUrl : "http://"+window.location.hostname+":8080",
panel_names : [],
default_route : '/dashboard/file/default.json',
grafana_index : 'grafana-dash',
elasticsearch_all_disabled : false,
timezoneOffset : null,
};
// This initializes a new hash on purpose, to avoid adding parameters to
@@ -27,16 +29,19 @@ function (_, crypto) {
settings[key] = typeof options[key] !== 'undefined' ? options[key] : defaults[key];
});
var url = settings.graphiteUrl;
var passwordAt = url.indexOf('@');
if (passwordAt > 0) {
var userStart = url.indexOf('//') + 2;
var userAndPassword = url.substring(userStart, passwordAt);
var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword);
var base64 = crypto.util.bytesToBase64(bytes);
settings.graphiteBasicAuth = base64;
}
var basicAuth = function(url) {
var passwordAt = url.indexOf('@');
if (passwordAt > 0) {
var userStart = url.indexOf('//') + 2;
var userAndPassword = url.substring(userStart, passwordAt);
var bytes = crypto.charenc.Binary.stringToBytes(userAndPassword);
var base64 = crypto.util.bytesToBase64(bytes);
return base64;
}
};
settings.graphiteBasicAuth = basicAuth(settings.graphiteUrl);
settings.elasticsearchBasicAuth = basicAuth(settings.elasticsearch);
return settings;
};
});

View File

@@ -58,7 +58,7 @@ function (angular, $, config, _) {
$scope.reset_row();
$scope.ejs = ejsResource(config.elasticsearch);
$scope.ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
$scope.bindKeyboardShortcuts();
};
@@ -72,6 +72,12 @@ function (angular, $, config, _) {
$rootScope.fullscreen = false;
});
$rootScope.$on('dashboard-saved', function() {
if ($rootScope.fullscreen) {
$rootScope.$emit('panel-fullscreen-exit');
}
});
keyboardManager.bind('ctrl+f', function(evt) {
$rootScope.$emit('open-search', evt);
}, { inputDisabled: true });

View File

@@ -65,17 +65,18 @@ function (angular, _, moment) {
type,
($scope.elasticsearch.title || dashboard.current.title),
($scope.loader.save_temp_ttl_enable ? ttl : false)
).then(
function(result) {
if(!_.isUndefined(result._id)) {
alertSrv.set('Dashboard Saved','This dashboard has been saved to Elasticsearch as "' +
result._id + '"','success',5000);
if(type === 'temp') {
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
}
} else {
).then(function(result) {
if(_.isUndefined(result._id)) {
alertSrv.set('Save failed','Dashboard could not be saved to Elasticsearch','error',5000);
return;
}
alertSrv.set('Dashboard Saved', 'This dashboard has been saved to Elasticsearch as "' + result._id + '"','success', 5000);
if(type === 'temp') {
$scope.share = dashboard.share_link(dashboard.current.title,'temp',result._id);
}
$rootScope.$emit('dashboard-saved');
});
};

View File

@@ -65,11 +65,13 @@ function (angular, app, _) {
_.each(state.graphs, function(graph) {
if (currentRow.panels.length === graphsPerRow) {
currentRow = angular.copy(rowTemplate);
newDashboard.rows.push(currentRow);
}
panel = {
type: 'graphite',
span: 12 / graphsPerRow,
title: graph[1].title,
targets: []
};
@@ -87,4 +89,4 @@ function (angular, app, _) {
});
});
});

View File

@@ -10,7 +10,13 @@ function (angular, _, config) {
module.controller('MetricKeysCtrl', function($scope, $http, $q) {
var elasticSearchUrlForMetricIndex = config.elasticsearch + '/' + config.grafana_metrics_index + '/';
var httpOptions = {};
if (config.elasticsearchBasicAuth) {
httpOptions.withCredentials = true;
httpOptions.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
$scope.init = function () {
$scope.metricPath = "prod.apps.api.boobarella.*";
$scope.metricCounter = 0;
@@ -77,7 +83,7 @@ function (angular, _, config) {
function deleteIndex()
{
var deferred = $q.defer();
$http.delete(elasticSearchUrlForMetricIndex)
$http.delete(elasticSearchUrlForMetricIndex, httpOptions)
.success(function() {
deferred.resolve('ok');
})
@@ -124,7 +130,7 @@ function (angular, _, config) {
}
}
}
});
}, httpOptions);
}
function receiveMetric(result) {

View File

@@ -51,12 +51,6 @@ function (angular, _, config, $) {
});
};
$scope.toggleImport = function ($event) {
$event.stopPropagation();
$scope.showImport = !$scope.showImport;
};
$scope.elasticsearch_dblist = function(queryStr) {
$scope.showImport = false;
$scope.selectedIndex = -1;
@@ -120,6 +114,15 @@ function (angular, _, config, $) {
});
};
$scope.toggleImport = function ($event) {
$event.stopPropagation();
$scope.showImport = !$scope.showImport;
};
$scope.newDashboard = function() {
$location.url('/dashboard/file/empty.json');
};
});

View File

@@ -1,5 +1,5 @@
{
"title": "New Dashboard",
"title": "Welcome to Grafana!",
"services": {
"filter": {
"list": [],
@@ -24,7 +24,7 @@
"type": "text",
"loadingEditor": false,
"mode": "markdown",
"content": "####Thank you for trying out Grafana! \n\nGeneral documentation is found in the readme and in the wiki section of the [Github Probject](https://github.com/torkelo/grafana). If you encounter any problem or have an idea for an improvement do not hesitate to open a github issue. \n\nTips: \n\n- Ctrl+S saves the current dashboard\n- Ctrl+F Opens the dashboard finder (searches elastic search)\n- Ctrl+H Hide/show row controls \n- Click and drag graph title to move panel (only works when row controls are enabled)\n\nIf you do not see a graph in the panel bellow the browser cannot access your graphite installation. Please make sure that the graphiteUrl property in config.js is correctly set and accessible.",
"content": "####Thank you for trying out Grafana! \n\nGeneral documentation is found in the readme and in the wiki section of the [Github Project](https://github.com/torkelo/grafana). If you encounter any problem or have an idea for an improvement do not hesitate to open a github issue. \n\nTips: \n\n- Ctrl+S saves the current dashboard\n- Ctrl+F Opens the dashboard finder (searches elastic search)\n- Ctrl+H Hide/show row controls \n- Click and drag graph title to move panel (only works when row controls are enabled)\n\nIf you do not see a graph in the panel bellow the browser cannot access your graphite installation. Please make sure that the graphiteUrl property in config.js is correctly set and accessible.",
"style": {},
"title": "Welcome to Grafana"
}
@@ -46,39 +46,12 @@
"x-axis": true,
"y-axis": true,
"scale": 1,
"y_format": "none",
"y2_format": "none",
"y_formats": ["short", "short"],
"grid": {
"max": null,
"min": 0
},
"annotate": {
"enable": false,
"query": "*",
"size": 20,
"field": "_type",
"sort": [
"_score",
"desc"
]
},
"auto_int": true,
"resolution": 100,
"interval": "5m",
"intervals": [
"auto",
"1s",
"1m",
"5m",
"10m",
"30m",
"1h",
"3h",
"12h",
"1d",
"1w",
"1y"
],
"lines": true,
"fill": 1,
"linewidth": 2,
@@ -87,7 +60,6 @@
"bars": false,
"stack": true,
"spyable": true,
"zoomlinks": false,
"options": false,
"legend": true,
"interactive": true,

View File

@@ -0,0 +1,83 @@
{
"title": "New Dashboard",
"services": {
"filter": {
"list": [],
"time": {
"from": "now-6h",
"to": "now"
}
}
},
"rows": [
{
"title": "Row1",
"height": "250px",
"editable": true,
"collapse": false,
"collapsable": true,
"panels": [],
"notice": false
}
],
"editable": true,
"failover": false,
"panel_hints": true,
"style": "dark",
"pulldowns": [
{
"type": "filtering",
"collapse": false,
"notice": false,
"enable": false
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
"notice": false,
"enable": true,
"status": "Stable",
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
],
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"now": true
}
],
"loader": {
"save_gist": false,
"save_elasticsearch": true,
"save_local": true,
"save_default": true,
"save_temp": true,
"save_temp_ttl_enable": true,
"save_temp_ttl": "30d",
"load_gist": false,
"load_elasticsearch": true,
"load_elasticsearch_size": 20,
"load_local": false,
"hide": false
},
"refresh": false
}

View File

@@ -8,5 +8,6 @@ define([
'./ngModelOnBlur',
'./tip',
'./confirmClick',
'./configModal'
'./configModal',
'./grafanaGraph'
], function () {});

View File

@@ -0,0 +1,323 @@
define([
'angular',
'jquery',
'kbn',
'moment',
'underscore'
],
function (angular, $, kbn, moment, _) {
'use strict';
var module = angular.module('kibana.directives');
module.directive('grafanaGraph', function(filterSrv, $rootScope, dashboard) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot;
var hiddenData = {};
scope.$on('refresh',function() {
if (scope.otherPanelInFullscreenMode()) { return; }
scope.get_data();
});
scope.$on('toggleLegend', function(e, alias) {
if (hiddenData[alias]) {
data.push(hiddenData[alias]);
delete hiddenData[alias];
}
render_panel();
});
// Receive render events
scope.$on('render',function(event, d) {
data = d || data;
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function() {
render_panel();
});
function setElementHeight() {
try {
elem.css({ height: scope.height || scope.panel.height || scope.row.height });
return true;
} catch(e) { // IE throws errors sometimes
return false;
}
}
// Function for rendering panel
function render_panel() {
if (!data) { return; }
if (scope.otherPanelInFullscreenMode()) { return; }
if (!setElementHeight()) { return; }
if (_.isString(data)) {
render_panel_as_graphite_png(data);
return;
}
_.each(_.keys(scope.hiddenSeries), function(seriesAlias) {
var dataSeries = _.find(data, function(series) {
return series.info.alias === seriesAlias;
});
if (dataSeries) {
hiddenData[dataSeries.info.alias] = dataSeries;
data = _.without(data, dataSeries);
}
});
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.interval);
var stack = scope.panel.stack ? true : null;
// Populate element
var options = {
legend: { show: false },
series: {
stackpercent: scope.panel.stack ? scope.panel.percentage : false,
stack: scope.panel.percentage ? null : stack,
lines: {
show: scope.panel.lines,
// Silly, but fixes bug in stacked percentages
fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10,
lineWidth: scope.panel.linewidth,
steps: scope.panel.steppedLine
},
bars: {
show: scope.panel.bars,
fill: 1,
barWidth: barwidth/1.5,
zero: false,
lineWidth: 0
},
points: {
show: scope.panel.points,
fill: 1,
fillColor: false,
radius: scope.panel.pointradius
},
shadowSize: 1
},
yaxes: [],
xaxis: {
timezone: dashboard.current.timezone,
show: scope.panel['x-axis'],
mode: "time",
min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
timeformat: time_format(scope.interval),
label: "Datetime",
ticks: elem.width()/100
},
grid: {
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8'
},
selection: {
mode: "x",
color: '#666'
}
};
addAnnotations(options);
for (var i = 0; i < data.length; i++) {
var _d = data[i].getFlotPairs(scope.panel.nullPointMode);
data[i].data = _d;
}
configureAxisOptions(data, options);
plot = $.plot(elem, data, options);
addAxisLabels();
}
function render_panel_as_graphite_png(url) {
url += '&width=' + elem.width();
url += '&height=' + elem.css('height').replace('px', '');
url += '&bgcolor=1f1f1f'; // @grayDarker & @kibanaPanelBackground
url += '&fgcolor=BBBFC2'; // @textColor & @grayLighter
url += scope.panel.stack ? '&areaMode=stacked' : '';
url += scope.panel.fill !== 0 ? ('&areaAlpha=' + (scope.panel.fill/10).toFixed(1)) : '';
url += scope.panel.linewidth !== 0 ? '&lineWidth=' + scope.panel.linewidth : '';
url += scope.panel.legend ? '' : '&hideLegend=true';
url += scope.panel.grid.min ? '&yMin=' + scope.panel.grid.min : '';
url += scope.panel.grid.max ? '&yMax=' + scope.panel.grid.max : '';
url += scope.panel['x-axis'] ? '' : '&hideAxes=true';
url += scope.panel['y-axis'] ? '' : '&hideYAxis=true';
switch(scope.panel.y_formats[0]) {
case 'bytes':
url += '&yUnitSystem=binary';
break;
case 'short':
url += '&yUnitSystem=si';
break;
case 'none':
url += '&yUnitSystem=none';
break;
}
switch(scope.panel.nullPointMode) {
case 'connected':
url += '&lineMode=connected';
break;
case 'null':
break; // graphite default lineMode
case 'null as zero':
url += "&drawNullAsZero=true";
break;
}
url += scope.panel.steppedLine ? '&lineMode=staircase' : '';
elem.html('<img src="' + url + '"></img>');
}
function addAnnotations(options) {
if(scope.panel.annotate.enable) {
options.events = {
levels: 1,
data: scope.annotations,
types: {
'annotation': {
level: 1,
icon: {
icon: "icon-tag icon-flip-vertical",
size: 20,
color: "#222",
outline: "#bbb"
}
}
}
};
}
}
function addAxisLabels() {
if (scope.panel.leftYAxisLabel) {
elem.css('margin-left', '10px');
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
.text(scope.panel.leftYAxisLabel)
.appendTo(elem);
yaxisLabel.css("margin-top", yaxisLabel.width() / 2 - 20);
} else if (elem.css('margin-left')) {
elem.css('margin-left', '');
}
}
function configureAxisOptions(data, options) {
var defaults = {
position: 'left',
show: scope.panel['y-axis'],
min: scope.panel.grid.min,
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max,
};
options.yaxes.push(defaults);
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.position = 'right';
options.yaxes.push(secondY);
configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]);
}
configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]);
}
function configureAxisMode(axis, format) {
if (format === 'bytes') {
axis.mode = "byte";
}
if (format === 'short') {
axis.tickFormatter = function(val) {
return kbn.shortFormat(val,0);
};
}
if (format === 'ms') {
axis.tickFormatter = kbn.msFormat;
}
}
function time_format(interval) {
var _int = kbn.interval_to_seconds(interval);
if(_int >= 2628000) {
return "%Y-%m";
}
if(_int >= 10000) {
return "%m/%d";
}
if(_int >= 3600) {
return "%m/%d %H:%M";
}
if(_int >= 700) {
return "%a %H:%M";
}
return "%H:%M";
}
var $tooltip = $('<div>');
elem.bind("plothover", function (event, pos, item) {
var group, value, timestamp;
if (item) {
if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
(item.series.info.alias || item.series.info.query)+
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ?
item.datapoint[1] - item.datapoint[2] :
item.datapoint[1];
if(item.series.info.y_format === 'bytes') {
value = kbn.byteFormat(value, 2);
}
if(item.series.info.y_format === 'short') {
value = kbn.shortFormat(value, 2);
}
if(item.series.info.y_format === 'ms') {
value = kbn.msFormat(value);
}
timestamp = dashboard.current.timezone === 'browser' ?
moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
$tooltip
.html(
group + value + " @ " + timestamp
)
.place_tt(pos.pageX, pos.pageY);
} else {
$tooltip.detach();
}
});
elem.bind("plotselected", function (event, ranges) {
filterSrv.setTime({
from : moment.utc(ranges.xaxis.from).toDate(),
to : moment.utc(ranges.xaxis.to).toDate(),
});
});
}
};
});
});

View File

@@ -40,7 +40,7 @@ function (angular, $) {
'onStop:\'panelMoveStop\''+
'}" ng-model="row.panels" ' +
'>' +
'{{panel.title}}' +
'{{panel.title || "No title"}}' +
'</span>' +
'</span>'+

View File

@@ -1,14 +1,82 @@
<div class="editor-row">
<div class="editor-row">
<div class="section">
<h5>Axes</h5>
<div class="editor-option">
<label class="small">Left y-axis label</label>
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-large" ng-model="panel.leftYAxisLabel">
<label class="small">X-Axis</label><input type="checkbox" ng-model="panel['x-axis']" ng-checked="panel['x-axis']" ng-change="render()">
</div>
<div class="editor-option">
<label class="small">Right y-axis label</label>
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-large" ng-model="panel.rightYAxisLabel">
<label class="small">Y-Axis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
</div>
<div class="editor-option">
<label class="small">Left Y Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_formats[1]" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Right Y Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_formats[2]" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Left Y-axis label</label>
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-medium" ng-model="panel.leftYAxisLabel">
</div>
<div class="editor-option">
<label class="small">Right Y-axis label</label>
<input ng-change="get_data()" ng-model-onblur placeholder="" type="text" class="input-medium" ng-model="panel.rightYAxisLabel">
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Grid</h5>
<div class="editor-option">
<label class="small">Min / <a ng-click="toggleGridMinMax('min')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.min)"></i></a></label>
<input type="number" class="input-small" ng-model="panel.grid.min" ng-change="render()" ng-model-onblur />
</div>
<div class="editor-option">
<label class="small">Max / <a ng-click="toggleGridMinMax('max')">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.max)"></i></a></label>
<input type="number" class="input-small" ng-model="panel.grid.max" ng-change="render()" ng-model-onblur />
</div>
</div>
<div class="section">
<h5>Legend</h5>
<div class="editor-option">
<label class="small">Show Legend</label><input type="checkbox" ng-model="panel.legend.show" ng-checked="panel.legend.show" ng-change="render();">
</div>
<div class="editor-option">
<label class="small">Include Values</label><input type="checkbox" ng-model="panel.legend.values" ng-checked="panel.legend.values" ng-change="render();">
</div>
</div>
<div class="section" ng-if="panel.legend.values">
<h5>Legend values</h5>
<div class="editor-option">
<label class="small">Min</label><input type="checkbox" ng-model="panel.legend.min" ng-checked="panel.legend.min" ng-change="render();">
</div>
<div class="editor-option">
<label class="small">Max</label><input type="checkbox" ng-model="panel.legend.max" ng-checked="panel.legend.max" ng-change="render();">
</div>
<div class="editor-option">
<label class="small">Current</label><input type="checkbox" ng-model="panel.legend.current" ng-checked="panel.legend.current" ng-change="render();">
</div>
<div class="editor-option">
<label class="small">Total</label><input type="checkbox" ng-model="panel.legend.total" ng-checked="panel.legend.total" ng-change="render();">
</div>
<div class="editor-option">
<label class="small">Avg</label><input type="checkbox" ng-model="panel.legend.avg" ng-checked="panel.legend.avg" ng-change="render();">
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<span ng-show="panel.legend"
<span ng-show="panel.legend.show"
ng-class="{'pull-right': series.yaxis === 2, 'hidden-series': hiddenSeries[series.alias]}"
ng-repeat='series in legend'
class="histogram-legend">
@@ -11,6 +11,23 @@
<a ng-click="toggleSeries(series)" data-unique="1" data-placement="{{series.yaxis === 2 ? 'bottomRight' : 'bottomLeft'}}">
{{series.alias}}
</a>
<span ng-if="panel.legend.values">
<span ng-show="panel.legend.current">
&nbsp;&nbsp;Current: {{series.current}}&nbsp;
</span>
<span ng-show="panel.legend.min">
&nbsp;&nbsp;Min: {{series.min}}&nbsp;
</span>
<span ng-show="panel.legend.max">
&nbsp;&nbsp;Max: {{series.max}}&nbsp;
</span>
<span ng-show="panel.legend.total">
&nbsp;&nbsp;Total: {{series.total}}&nbsp;
</span>
<span ng-show="panel.legend.avg">
&nbsp;&nbsp;Avg: {{series.avg}}&nbsp;
</span>
</span>
</span>
</span>

View File

@@ -12,7 +12,7 @@
<center><img ng-show='panel.loading && _.isUndefined(data)' src="img/load_big.gif"></center>
<div histogram-chart class="pointer histogram-chart" params="{{panel}}">
<div grafana-graph class="pointer histogram-chart">
</div>
<div ng-if="panel.legend" class="grafana-legend-container">
@@ -24,9 +24,11 @@
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
</div>
</div>
<div class="tab-content" ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></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>

View File

@@ -46,19 +46,19 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
src:'app/panels/graphite/editor.html'
},
{
title:'Axis labels',
title:'Axes & Grid',
src:'app/panels/graphite/axisEditor.html'
},
{
title:'Style',
title:'Display Styles',
src:'app/panels/graphite/styleEditor.html'
}
],
menuItems: [
{ text: 'View fullscreen', click: 'toggleFullscreen()' },
{ text: 'Edit', click: 'openConfigureModal()' },
{ text: 'Duplicate', click: 'duplicate()' },
{ text: 'Edit', click: 'openConfigureModal()' },
{ text: 'Fullscreen', click: 'toggleFullscreen()' },
{ text: 'Duplicate', click: 'duplicate()' },
{ text: 'Span', submenu: [
{ text: '1', click: 'updateColumnSpan(1)' },
{ text: '2', click: 'updateColumnSpan(2)' },
@@ -82,6 +82,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
// Set and populate defaults
var _d = {
/** @scratch /panels/histogram/3
* renderer:: sets client side (flot) or native graphite png renderer (png)
*/
renderer: 'flot',
/** @scratch /panels/histogram/3
* x-axis:: Show the x-axis
*/
@@ -95,53 +99,28 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
*/
scale : 1,
/** @scratch /panels/histogram/3
* y_format:: 'none','bytes','short '
* y_formats :: 'none','bytes','short', 'ms'
*/
y_format : 'none',
y2_format : 'none',
y_formats : ['short', 'short'],
/** @scratch /panels/histogram/5
* grid object:: Min and max y-axis values
* grid.min::: Minimum y-axis value
* grid.max::: Maximum y-axis value
* grid.ma1::: Maximum y-axis value
*/
grid : {
max: null,
min: 0
},
/** @scratch /panels/histogram/3
* ==== Annotations
* annotate object:: A query can be specified, the results of which will be displayed as markers on
* the chart. For example, for noting code deploys.
* annotate.enable::: Should annotations, aka markers, be shown?
* annotate.query::: Lucene query_string syntax query to use for markers.
* annotate.size::: Max number of markers to show
* annotate.field::: Field from documents to show
* annotate.sort::: Sort array in format [field,order], For example [`@timestamp',`desc']
*/
annotate : {
enable : false,
query : "*",
size : 20,
field : '_type',
sort : ['_score','desc']
},
/** @scratch /panels/histogram/3
* ==== Interval options
* auto_int:: Automatically scale intervals?
*/
auto_int : true,
/** @scratch /panels/histogram/3
* resolution:: If auto_int is true, shoot for this many bars.
*/
resolution : 100,
/** @scratch /panels/histogram/3
* interval:: If auto_int is set to false, use this as the interval.
*/
interval : '5m',
/** @scratch /panels/histogram/3
* interval:: Array of possible intervals in the *View* selector. Example [`auto',`1s',`5m',`3h']
*/
intervals : ['auto','1s','1m','5m','10m','30m','1h','3h','12h','1d','1w','1y'],
/** @scratch /panels/histogram/3
* ==== Drawing options
* lines:: Show line chart
@@ -171,35 +150,20 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
* stack:: Stack multiple series
*/
stack : true,
/** @scratch /panels/histogram/3
* spyable:: Show inspect icon
*/
spyable : true,
/** @scratch /panels/histogram/3
* zoomlinks:: Show `Zoom Out' link
*/
zoomlinks : false,
/** @scratch /panels/histogram/3
* options:: Show quick view options section
*/
options : false,
/** @scratch /panels/histogram/3
* legend:: Display the legond
*/
legend : true,
/** @scratch /panels/histogram/3
* interactive:: Enable click-and-drag to zoom functionality
*/
interactive : true,
/** @scratch /panels/histogram/3
* legend_counts:: Show counts in legend
*/
legend_counts : true,
legend: {
show: true, // disable/enable legend
values: false, // disable/enable legend values
min: false,
max: false,
current: false,
total: false,
avg: false
},
/** @scratch /panels/histogram/3
* ==== Transformations
* timezone:: Correct for browser timezone?. Valid values: browser, utc
*/
timezone : 'browser', // browser or utc
/** @scratch /panels/histogram/3
* percentage:: Show the y-axis as a percentage of the axis total. Only makes sense for multiple
* queries
@@ -211,6 +175,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
zerofill : true,
nullPointMode : 'connected',
steppedLine: false,
tooltip : {
@@ -225,13 +190,26 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
};
_.defaults($scope.panel,_d);
_.defaults($scope.panel.tooltip,_d.tooltip);
_.defaults($scope.panel.annotate,_d.annotate);
_.defaults($scope.panel.grid,_d.grid);
_.defaults($scope.panel.tooltip, _d.tooltip);
_.defaults($scope.panel.annotate, _d.annotate);
_.defaults($scope.panel.grid, _d.grid);
// backward compatible stuff
if (_.isBoolean($scope.panel.legend)) {
$scope.panel.legend = { show: $scope.panel.legend };
_.defaults($scope.panel.legend, _d.legend);
}
if ($scope.panel.y_format) {
$scope.panel.y_formats[0] = $scope.panel.y_format;
delete $scope.panel.y_format;
}
if ($scope.panel.y2_format) {
$scope.panel.y_formats[1] = $scope.panel.y2_format;
delete $scope.panel.y2_format;
}
$scope.init = function() {
// Hide view options by default
$scope.fullscreen = false;
$scope.options = false;
@@ -246,19 +224,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
};
$scope.set_interval = function(interval) {
if(interval !== 'auto') {
$scope.panel.auto_int = false;
$scope.panel.interval = interval;
} else {
$scope.panel.auto_int = true;
}
};
$scope.typeAheadSource = function () {
return ["test", "asd", "testing2"];
};
$scope.remove_panel_from_row = function(row, panel) {
if ($scope.fullscreen) {
$rootScope.$emit('panel-fullscreen-exit');
@@ -273,24 +238,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.get_data();
};
$scope.interval_label = function(interval) {
return $scope.panel.auto_int && interval === $scope.panel.interval ? interval+" (auto)" : interval;
};
$scope.updateTimeRange = function () {
var range = filterSrv.timeRange();
var interval = filterSrv.timeRange();
$scope.range = filterSrv.timeRange();
$scope.interval = '10m';
if ($scope.panel.auto_int) {
if (range) {
interval = kbn.secondsToHms(
kbn.calculate_interval(range.from, range.to, $scope.panel.resolution, 0) / 1000
);
}
if ($scope.range) {
$scope.interval = kbn.secondsToHms(
kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.panel.resolution, 0) / 1000
);
}
$scope.interval = $scope.panel.interval = interval || '10m';
$scope.range = range;
};
$scope.colors = [
@@ -322,6 +278,7 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var graphiteQuery = {
range: filterSrv.timeRange(false),
targets: $scope.panel.targets,
renderer: $scope.panel.renderer,
maxDataPoints: $scope.panel.span * 50
};
@@ -334,9 +291,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.receiveGraphiteData = function(results) {
$scope.panelMeta.loading = false;
$scope.legend = [];
// png renderer returns just a url
if (_.isString(results)) {
$scope.render(results);
return;
}
results = results.data;
$scope.legend = [];
var data = [];
_.each(results, function(targetData) {
@@ -344,34 +307,21 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
var color = $scope.panel.aliasColors[alias] || $scope.colors[data.length];
var yaxis = $scope.panel.aliasYAxis[alias] || 1;
var tsOpts = {
interval: $scope.interval,
start_date: $scope.range && $scope.range.from,
end_date: $scope.range && $scope.range.to,
};
var time_series = new timeSeries.ZeroFilled(tsOpts);
_.each(targetData.datapoints, function(valueArray) {
if (valueArray[0]) {
time_series.addValue(valueArray[1] * 1000, valueArray[0]);
}
});
var seriesInfo = {
alias: alias,
color: color,
enable: true,
yaxis: yaxis,
y_format: $scope.panel.y_formats[yaxis - 1]
};
$scope.legend.push(seriesInfo);
data.push({
var series = new timeSeries.ZeroFilled({
datapoints: targetData.datapoints,
info: seriesInfo,
time_series: time_series,
});
$scope.legend.push(seriesInfo);
data.push(series);
});
$scope.render(data);
@@ -425,6 +375,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.enterFullscreenMode({edit: true});
};
$scope.otherPanelInFullscreenMode = function() {
return $rootScope.fullscreen && !$scope.fullscreen;
};
$scope.render = function(data) {
$scope.$emit('render', data);
};
@@ -482,6 +436,11 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
$scope.render();
};
$scope.toggleGridMinMax = function(key) {
$scope.panel.grid[key] = _.toggle($scope.panel.grid[key], null, 0);
$scope.render();
};
$scope.updateColumnSpan = function(span) {
$scope.panel.span = span;
$timeout($scope.render);
@@ -489,275 +448,6 @@ function (angular, app, $, _, kbn, moment, timeSeries) {
});
module.directive('histogramChart', function(filterSrv, $rootScope) {
return {
restrict: 'A',
template: '<div> </div>',
link: function(scope, elem) {
var data, plot;
var hiddenData = {};
scope.$on('refresh',function() {
if ($rootScope.fullscreen && !scope.fullscreen) {
return;
}
scope.get_data();
});
scope.$on('toggleLegend', function(e, alias) {
if (hiddenData[alias]) {
data.push(hiddenData[alias]);
delete hiddenData[alias];
}
render_panel();
});
// Receive render events
scope.$on('render',function(event, d) {
data = d || data;
render_panel();
});
// Re-render if the window is resized
angular.element(window).bind('resize', function() {
render_panel();
});
// Function for rendering panel
function render_panel() {
if (!data) {
return;
}
try {
elem.css({ height: scope.height || scope.panel.height || scope.row.height });
} catch(e) { return; }
_.each(data, function(series) {
series.label = series.info.alias;
series.color = series.info.color;
});
_.each(_.keys(scope.hiddenSeries), function(seriesAlias) {
var dataSeries = _.find(data, function(series) {
return series.info.alias === seriesAlias;
});
if (dataSeries) {
hiddenData[dataSeries.info.alias] = dataSeries;
data = _.without(data, dataSeries);
}
});
// Set barwidth based on specified interval
var barwidth = kbn.interval_to_ms(scope.panel.interval);
var stack = scope.panel.stack ? true : null;
// Populate element
var options = {
legend: { show: false },
series: {
stackpercent: scope.panel.stack ? scope.panel.percentage : false,
stack: scope.panel.percentage ? null : stack,
lines: {
show: scope.panel.lines,
// Silly, but fixes bug in stacked percentages
fill: scope.panel.fill === 0 ? 0.001 : scope.panel.fill/10,
lineWidth: scope.panel.linewidth,
steps: scope.panel.steppedLine
},
bars: {
show: scope.panel.bars,
fill: 1,
barWidth: barwidth/1.5,
zero: false,
lineWidth: 0
},
points: {
show: scope.panel.points,
fill: 1,
fillColor: false,
radius: scope.panel.pointradius
},
shadowSize: 1
},
yaxes: [],
xaxis: {
timezone: scope.panel.timezone,
show: scope.panel['x-axis'],
mode: "time",
min: _.isUndefined(scope.range.from) ? null : scope.range.from.getTime(),
max: _.isUndefined(scope.range.to) ? null : scope.range.to.getTime(),
timeformat: time_format(scope.panel.interval),
label: "Datetime",
ticks: elem.width()/100
},
grid: {
backgroundColor: null,
borderWidth: 0,
hoverable: true,
color: '#c8c8c8'
}
};
if(scope.panel.annotate.enable) {
options.events = {
levels: 1,
data: scope.annotations,
types: {
'annotation': {
level: 1,
icon: {
icon: "icon-tag icon-flip-vertical",
size: 20,
color: "#222",
outline: "#bbb"
}
}
}
//xaxis: int // the x axis to attach events to
};
}
if(scope.panel.interactive) {
options.selection = { mode: "x", color: '#666' };
}
// when rendering stacked bars, we need to ensure each point that has data is zero-filled
// so that the stacking happens in the proper order
var required_times = [];
if (data.length > 1) {
required_times = Array.prototype.concat.apply([], _.map(data, function (query) {
return query.time_series.getOrderedTimes();
}));
required_times = _.uniq(required_times.sort(function (a, b) {
// decending numeric sort
return a-b;
}), true);
}
for (var i = 0; i < data.length; i++) {
var _d = data[i].time_series.getFlotPairs(required_times, scope.panel.nullPointMode);
data[i].yaxis = data[i].info.yaxis;
data[i].data = _d;
data[i].info.y_format = data[i].yaxis === 1 ? scope.panel.y_format : scope.panel.y2_format;
}
configureAxisOptions(data, options);
plot = $.plot(elem, data, options);
if (scope.panel.leftYAxisLabel) {
elem.css('margin-left', '10px');
var yaxisLabel = $("<div class='axisLabel yaxisLabel'></div>")
.text(scope.panel.leftYAxisLabel)
.appendTo(elem);
yaxisLabel.css("margin-top", yaxisLabel.width() / 2 - 20);
} else if (elem.css('margin-left')) {
elem.css('margin-left', '');
}
}
function configureAxisOptions(data, options)
{
var defaults = {
position: 'left',
show: scope.panel['y-axis'],
min: scope.panel.grid.min,
max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.max
};
options.yaxes.push(defaults);
if (_.findWhere(data, {yaxis: 2})) {
var secondY = _.clone(defaults);
secondY.position = 'right';
options.yaxes.push(secondY);
configureAxisMode(options.yaxes[1], scope.panel.y2_format);
}
configureAxisMode(options.yaxes[0], scope.panel.y_format);
}
function configureAxisMode(axis, format) {
if (format === 'bytes') {
axis.mode = "byte";
}
if (format === 'short') {
axis.tickFormatter = function(val) {
return kbn.shortFormat(val,0);
};
}
if (format === 'ms') {
axis.tickFormatter = kbn.msFormat;
}
}
function time_format(interval) {
var _int = kbn.interval_to_seconds(interval);
if(_int >= 2628000) {
return "%Y-%m";
}
if(_int >= 10000) {
return "%Y-%m-%d";
}
if(_int >= 60) {
return "%H:%M<br>%m-%d";
}
return "%H:%M:%S";
}
var $tooltip = $('<div>');
elem.bind("plothover", function (event, pos, item) {
var group, value, timestamp;
if (item) {
if (item.series.info.alias || scope.panel.tooltip.query_as_alias) {
group = '<small style="font-size:0.9em;">' +
'<i class="icon-circle" style="color:'+item.series.color+';"></i>' + ' ' +
(item.series.info.alias || item.series.info.query)+
'</small><br>';
} else {
group = kbn.query_color_dot(item.series.color, 15) + ' ';
}
value = (scope.panel.stack && scope.panel.tooltip.value_type === 'individual') ?
item.datapoint[1] - item.datapoint[2] :
item.datapoint[1];
if(item.series.info.y_format === 'bytes') {
value = kbn.byteFormat(value,2);
}
if(item.series.info.y_format === 'short') {
value = kbn.shortFormat(value,2);
}
if(item.series.info.y_format === 'ms') {
value = kbn.msFormat(value);
}
timestamp = scope.panel.timezone === 'browser' ?
moment(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss') :
moment.utc(item.datapoint[0]).format('YYYY-MM-DD HH:mm:ss');
$tooltip
.html(
group + value + " @ " + timestamp
)
.place_tt(pos.pageX, pos.pageY);
} else {
$tooltip.detach();
}
});
elem.bind("plotselected", function (event, ranges) {
filterSrv.setTime({
from : moment.utc(ranges.xaxis.from).toDate(),
to : moment.utc(ranges.xaxis.to).toDate(),
});
});
}
};
});
});

View File

@@ -1,3 +1,5 @@
<div class="editor-row">
<div class="section">
<h5>Chart Options</h5>
@@ -10,31 +12,8 @@
<div class="editor-option">
<label class="small">Points</label><input type="checkbox" ng-model="panel.points" ng-checked="panel.points" ng-change="render()">
</div>
<div class="editor-option">
<label class="small">Selectable</label><input type="checkbox" ng-model="panel.interactive" ng-checked="panel.interactive">
</div>
</div>
<div class="section">
<h5>Axis</h5>
<div class="editor-option">
<label class="small">xAxis</label><input type="checkbox" ng-model="panel['x-axis']" ng-checked="panel['x-axis']" ng-change="render()">
</div>
<div class="editor-option">
<label class="small">yAxis</label><input type="checkbox" ng-model="panel['y-axis']" ng-checked="panel['y-axis']" ng-change="render()">
</div>
<div class="editor-option" ng-show="panel.points">
<label class="small">Point Radius</label>
<select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Left Y Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y_format" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Right Y Format <tip>Y-axis formatting</tip></label>
<select class="input-small" ng-model="panel.y2_format" ng-options="f for f in ['none','short','bytes', 'ms']" ng-change="render()"></select>
</div>
</div>
<div class="section">
<h5>Line options</h5>
<div class="editor-option" ng-show="panel.lines">
@@ -45,12 +24,16 @@
<label class="small">Line Width</label>
<select class="input-mini" ng-model="panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
</div>
<div class="editor-option" ng-show="panel.points">
<label class="small">Point Radius</label>
<select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Null point mode <tip>Define how null values should be drawn</tip></label>
<select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Stepped lines</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
<label class="small">Staircase line</label><input type="checkbox" ng-model="panel.steppedLine" ng-checked="panel.steppedLine" ng-change="render()">
</div>
</div>
<div class="section">
@@ -67,32 +50,17 @@
<select class="input-small" ng-model="panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="render()"></select>
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<h5>Legend<h5>
<div class="editor-option">
<label class="small">Legend</label><input type="checkbox" ng-model="panel.legend" ng-checked="panel.legend">
</div>
<div ng-show="panel.legend" class="editor-option">
<label class="small">Query <tip>If no alias is set, show the query in the legend</tip></label><input type="checkbox" ng-model="panel.show_query" ng-checked="panel.show_query">
</div>
<div ng-show="panel.legend" class="editor-option">
<label class="small">Counts</label><input type="checkbox" ng-model="panel.legend_counts" ng-checked="panel.legend_counts">
</div>
</div>
<div class="section">
<h5>Grid<h5>
<h5>Rendering</h5>
<div class="editor-option">
<label class="small">Min / <a href='' ng-click="panel.grid.min = _.toggle(panel.grid.min,null,0)">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.min)"></i></a></label>
<input type="number" class="input-small" ng-model="panel.grid.min"/>
<label class="small">Flot <tip>client side</tip></label>
<input type="radio" class="input-small" ng-model="panel.renderer" value="flot" ng-change="get_data()" />
</div>
<div class="editor-option">
<label class="small">Max / <a ref='' ng-click="panel.grid.max = _.toggle(panel.grid.max,null,0)">Auto <i class="icon-star" ng-show="_.isNull(panel.grid.max)"></i></a></label>
<input type="number" class="input-small" ng-model="panel.grid.max"/>
<label class="small">Graphite PNG <tip>server side</tip></label>
<input type="radio" class="input-small" ng-model="panel.renderer" value="png" ng-change="get_data()" />
</div>
</div>
</div>

View File

@@ -1,229 +1,61 @@
define([
'underscore',
'./interval'
'underscore'
],
function (_, Interval) {
function (_) {
'use strict';
var ts = {};
// map compatable parseInt
function base10Int(val) {
return parseInt(val, 10);
}
// trim the ms off of a time, but return it with empty ms.
function getDatesTime(date) {
return Math.floor(date.getTime() / 1000)*1000;
}
/**
* Certain graphs require 0 entries to be specified for them to render
* properly (like the line graph). So with this we will caluclate all of
* the expected time measurements, and fill the missing ones in with 0
* @param {object} opts An object specifying some/all of the options
*
* OPTIONS:
* @opt {string} interval The interval notion describing the expected spacing between
* each data point.
* @opt {date} start_date (optional) The start point for the time series, setting this and the
* end_date will ensure that the series streches to resemble the entire
* expected result
* @opt {date} end_date (optional) The end point for the time series, see start_date
*/
ts.ZeroFilled = function (opts) {
opts = _.defaults(opts, {
interval: '10m',
start_date: null,
end_date: null,
});
// the expected differenece between readings.
this.interval = new Interval(opts.interval);
// will keep all values here, keyed by their time
this._data = {};
this.start_time = opts.start_date && getDatesTime(opts.start_date);
this.end_time = opts.end_date && getDatesTime(opts.end_date);
this.opts = opts;
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
this.color = opts.info.color;
this.yaxis = opts.info.yaxis;
};
/**
* Add a row
* @param {int} time The time for the value, in
* @param {any} value The value at this time
*/
ts.ZeroFilled.prototype.addValue = function (time, value) {
if (time instanceof Date) {
time = getDatesTime(time);
} else {
time = base10Int(time);
}
if (!isNaN(time)) {
this._data[time] = (_.isUndefined(value) ? 0 : value);
}
this._cached_times = null;
};
ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle) {
var result = [];
/**
* Get an array of the times that have been explicitly set in the series
* @param {array} include (optional) list of timestamps to include in the response
* @return {array} An array of integer times.
*/
ts.ZeroFilled.prototype.getOrderedTimes = function (include) {
var times = _.map(_.keys(this._data), base10Int);
if (_.isArray(include)) {
times = times.concat(include);
}
return _.uniq(times.sort(function (a, b) {
// decending numeric sort
return a - b;
}), true);
};
this.info.total = 0;
this.info.max = null;
this.info.min = 212312321312;
/**
* return the rows in the format:
* [ [time, value], [time, value], ... ]
*
* Heavy lifting is done by _get(Min|Default|All)FlotPairs()
* @param {array} required_times An array of timestamps that must be in the resulting pairs
* @return {array}
*/
ts.ZeroFilled.prototype.getFlotPairs = function (required_times, fillStyle) {
var times = this.getOrderedTimes(required_times),
strategy,
pairs;
if(fillStyle === 'null as zero') {
strategy = this._getAllFlotPairs;
} else if(fillStyle === 'null') {
strategy = this._getNullFlotPairs;
} else if(fillStyle === 'connected') {
strategy = this._getNoZeroFlotPairs;
} else {
strategy = this._getMinFlotPairs;
}
pairs = _.reduce(
times, // what
strategy, // how
[], // where
this // context
);
// if the first or last pair is inside either the start or end time,
// add those times to the series with null values so the graph will stretch to contain them.
// Removing, flot 0.8.1's max/min params satisfy this
/*
if (this.start_time && (pairs.length === 0 || pairs[0][0] > this.start_time)) {
pairs.unshift([this.start_time, null]);
}
if (this.end_time && (pairs.length === 0 || pairs[pairs.length - 1][0] < this.end_time)) {
pairs.push([this.end_time, null]);
}
*/
return pairs;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Fill zero's on either side of the current time, unless there is already a measurement there or
* we are looking at an edge.
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getMinFlotPairs = function (result, time, i, times) {
var next, expected_next, prev, expected_prev;
// check for previous measurement
if (i > 0) {
prev = times[i - 1];
expected_prev = this.interval.before(time);
if (prev < expected_prev) {
result.push([expected_prev, 0]);
_.each(this.datapoints, function(valueArray) {
var currentTime = valueArray[1];
var currentValue = valueArray[0];
if (currentValue === null) {
if (fillStyle === 'connected') {
return;
}
if (fillStyle === 'null as zero') {
currentValue = 0;
}
}
}
// add the current time
result.push([ time, this._data[time] || 0]);
// check for next measurement
if (times.length > i) {
next = times[i + 1];
expected_next = this.interval.after(time);
if (next > expected_next) {
result.push([expected_next, 0]);
if (_.isNumber(currentValue)) {
this.info.total += currentValue;
}
if (currentValue > this.info.max) {
this.info.max = currentValue;
}
if (currentValue < this.info.min) {
this.info.min = currentValue;
}
result.push([currentTime * 1000, currentValue]);
}, this);
if (result.length) {
this.info.avg = (this.info.total / result.length).toFixed(2);
this.info.current = result[result.length-1][1];
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Fill zero's to the right of each time, until the next measurement is reached or we are at the
* last measurement
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getAllFlotPairs = function (result, time, i, times) {
var next, expected_next;
result.push([ times[i], this._data[times[i]] || 0 ]);
next = times[i + 1];
expected_next = this.interval.after(time);
for(; times.length > i && next > expected_next; expected_next = this.interval.after(expected_next)) {
result.push([expected_next, 0]);
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Same as min, but fills with nulls
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getNullFlotPairs = function (result, time, i, times) {
var next, expected_next, prev, expected_prev;
// check for previous measurement
if (i > 0) {
prev = times[i - 1];
expected_prev = this.interval.before(time);
if (prev < expected_prev) {
result.push([expected_prev, null]);
}
}
// add the current time
result.push([ time, this._data[time] || null]);
// check for next measurement
if (times.length > i) {
next = times[i + 1];
expected_next = this.interval.after(time);
if (next > expected_next) {
result.push([expected_next, null]);
}
}
return result;
};
/**
* ** called as a reduce stragegy in getFlotPairs() **
* Not fill zero's on either side of the current time, only the current time
* @return {array} An array of points to plot with flot
*/
ts.ZeroFilled.prototype._getNoZeroFlotPairs = function (result, time) {
// add the current time
if(this._data[time]){
result.push([ time, this._data[time]]);
}
return result;
};
return ts;
});

View File

@@ -2,7 +2,7 @@
<div class="pull-right editor-title">Dashboard settings</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General', 'Rows','Controls', 'Metrics', 'Import']" data-title="{{tab}}">
<div ng-repeat="tab in ['General', 'Rows','Controls', 'Import']" data-title="{{tab}}">
</div>
<div ng-repeat="tab in dashboard.current.nav" data-title="{{tab.type}}">
</div>
@@ -18,7 +18,8 @@
<label class="small">Style</label><select class="input-small" ng-model="dashboard.current.style" ng-options="f for f in ['dark','light']"></select>
</div>
<div class="editor-option">
<label class="small"> Editable </label><input type="checkbox" ng-model="dashboard.current.editable" ng-checked="dashboard.current.editable" />
<label class="small">Time correction</label>
<select ng-model="dashboard.current.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small"> Hints <tip>Show 'Add panel' hints in empty spaces</tip></label><input type="checkbox" ng-model="dashboard.current.panel_hints" ng-checked="dashboard.current.panel_hints" />

View File

@@ -1,5 +1,5 @@
<div ng-controller="GraphiteImportCtrl" ng-init="init()">
<h5>Import dashboards from graphite webb</h5>
<h5>Import dashboards from graphite web</h5>
<div class="editor-row">
<div class="section">

View File

@@ -19,12 +19,24 @@
<ul class="dropdown-menu" id="grafana-search">
<li ng-if="!showImport">
<div class="grafana-search-panel">
<input type="text"
placeholder="search dashboards, metrics, or graphs"
xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)"
ng-model="elasticsearch.query"
ng-change="elasticsearch_dblist(elasticsearch.query)" />
<div class="search-field-wrapper">
<button class="btn btn-success pull-right" ng-click="toggleImport($event)">
<i class="icon-download-alt"></i>
Import
</button>
<button class="btn btn-success pull-right" ng-click="newDashboard()">
<i class="icon-th-large"></i>
New
</button>
<span>
<input type="text"
placeholder="search dashboards, metrics, or graphs"
xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)"
ng-model="elasticsearch.query"
ng-change="elasticsearch_dblist(elasticsearch.query)" />
</span>
</div>
<h6 ng-hide="search_results.dashboards.length || search_results.metrics.length">No dashboards or metrics matching your query found</h6>
<table class="table table-condensed table-striped">
<tr bindonce ng-repeat="row in search_results.metrics"
@@ -52,9 +64,6 @@
</div>
</li>
<li class="pull-right" style="margin: 5px;">
<a ng-click="toggleImport($event)">Import</a>
</li>
<!-- ng-show="dashboard.current.loader.load_gist || dashboard.current.loader.load_local" -->
<li ng-if="showImport" style="margin: 20px;">
<div class="editor-row">

View File

@@ -22,6 +22,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
var _dash = {
title: "",
style: "dark",
timezone: 'browser',
editable: true,
failover: false,
panel_hints: true,
@@ -47,7 +48,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
};
// An elasticJS client to use
var ejs = ejsResource(config.elasticsearch);
var ejs = ejsResource(config.elasticsearch, config.elasticsearchBasicAuth);
var gist_pattern = /(^\d{5,}$)|(^[a-z0-9]{10,}$)|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/;
// Store a reference to this
@@ -101,8 +102,8 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
$location.path(config.default_route);
alertSrv.set('Saving to browser storage has been replaced',' with saving to Elasticsearch.'+
' Click <a href="#/dashboard/local/deprecated">here</a> to load your old dashboard anyway.');
} else if(!(_.isUndefined(window.localStorage.kibanaDashboardDefault))) {
$location.path(window.localStorage.kibanaDashboardDefault);
} else if(!(_.isUndefined(window.localStorage.grafanaDashboardDefault))) {
$location.path(window.localStorage.grafanaDashboardDefault);
} else {
$location.path(config.default_route);
}
@@ -196,7 +197,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
if(!_.isUndefined(window.localStorage['dashboard'])) {
delete window.localStorage['dashboard'];
}
window.localStorage.kibanaDashboardDefault = route;
window.localStorage.grafanaDashboardDefault = route;
return true;
} else {
return false;
@@ -210,7 +211,7 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
delete window.localStorage['dashboard'];
}
delete window.localStorage.kibanaDashboardDefault;
delete window.localStorage.grafanaDashboardDefault;
return true;
} else {
return false;
@@ -286,13 +287,21 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
};
this.elasticsearch_load = function(type,id) {
return $http({
var options = {
url: config.elasticsearch + "/" + config.grafana_index + "/"+type+"/"+id+'?' + new Date().getTime(),
method: "GET",
transformResponse: function(response) {
return renderTemplate(angular.fromJson(response)._source.dashboard, $routeParams);
}
}).error(function(data, status) {
};
if (config.elasticsearchBasicAuth) {
options.withCredentials = true;
options.headers = {
"Authorization": "Basic " + config.elasticsearchBasicAuth
};
}
return $http(options)
.error(function(data, status) {
if(status === 0) {
alertSrv.set('Error',"Could not contact Elasticsearch at "+config.elasticsearch+
". Please ensure that Elasticsearch is reachable from your system." ,'error');
@@ -377,10 +386,14 @@ function (angular, $, kbn, _, config, moment, Modernizr) {
);
};
this.elasticsearch_list = function(query,count) {
this.elasticsearch_list = function(query, count) {
var request = ejs.Request().indices(config.grafana_index).types('dashboard');
// if elasticsearch has disabled _all field we need
// need to specifiy field here
var q = 'title:' + (query || '*');
return request.query(
ejs.QueryStringQuery(query || '*')
ejs.QueryStringQuery(q)
).size(count).doSearch(
// Success
function(result) {

View File

@@ -101,6 +101,11 @@ function (_) {
defaultParams: [3]
});
addFuncDef({
name: 'sortByName',
category: categories.Special
});
addFuncDef({
name: 'aliasByMetric',
category: categories.Special,
@@ -138,10 +143,17 @@ function (_) {
});
addFuncDef({
name: 'derivate',
name: 'derivative',
category: categories.Transform,
});
addFuncDef({
name: 'nonNegativeDerivative',
category: categories.Transform,
params: [ { name: "max value or 0", type: "int", } ],
defaultParams: [0]
});
addFuncDef({
name: 'timeShift',
category: categories.Transform,
@@ -175,6 +187,13 @@ function (_) {
defaultParams: [25]
});
addFuncDef({
name: 'highestCurrent',
category: categories.Filter,
params: [ { name: "count", type: "int" } ],
defaultParams: [5]
});
_.each(categories, function(funcList, catName) {
categories[catName] = _.sortBy(funcList, 'name');
});
@@ -229,4 +248,4 @@ function (_) {
}
};
});
});

View File

@@ -3,14 +3,16 @@ define([
'underscore',
'jquery',
'config',
'kbn'
'kbn',
'moment'
],
function (angular, _, $, config, kbn) {
function (angular, _, $, config, kbn, moment) {
'use strict';
var module = angular.module('kibana.services');
module.service('graphiteSrv', function($http, $q, filterSrv) {
var graphiteRenderUrl = config.graphiteUrl + "/render";
this.query = function(options) {
try {
@@ -18,14 +20,19 @@ function (angular, _, $, config, kbn) {
from: this.translateTime(options.range.from),
until: this.translateTime(options.range.to),
targets: options.targets,
renderer: options.renderer,
maxDataPoints: options.maxDataPoints
};
var params = buildGraphitePostParams(graphOptions);
var params = buildGraphiteParams(graphOptions);
if (options.renderer === 'png') {
return $q.when(graphiteRenderUrl + '?' + params.join('&'));
}
return doGraphiteRequest({
method: 'POST',
url: config.graphiteUrl + '/render/',
url: graphiteRenderUrl,
data: params.join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
@@ -42,7 +49,7 @@ function (angular, _, $, config, kbn) {
if (date === 'now') {
return 'now';
}
else if (date.indexOf('now') > 0) {
else if (date.indexOf('now') >= 0) {
date = date.substring(3);
date = date.replace('m', 'min');
date = date.replace('M', 'mon');
@@ -52,7 +59,13 @@ function (angular, _, $, config, kbn) {
date = kbn.parseDate(date);
}
return $.plot.formatDate(date, '%H%:%M_%Y%m%d');
date = moment.utc(date).local();
if (config.timezoneOffset) {
date = date.zone(config.timezoneOffset);
}
return date.format('HH:mm_YYYYMMDD');
};
this.match = function(targets, graphiteTargetStr) {
@@ -115,11 +128,13 @@ function (angular, _, $, config, kbn) {
return $http(options);
}
function buildGraphitePostParams(options) {
function buildGraphiteParams(options) {
var clean_options = [];
var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints'];
options['format'] = 'json';
if (options.renderer !== 'png') {
options['format'] = 'json';
}
$.each(options, function (key, value) {
if ($.inArray(key, graphite_options) === -1) {
@@ -143,4 +158,4 @@ function (angular, _, $, config, kbn) {
});
});
});

View File

@@ -116,6 +116,7 @@ define([
for (var i = 0; i < 128; i++) {
identifierStartTable[i] =
i >= 48 && i <= 57 || // 0-9
i === 36 || // $
i >= 65 && i <= 90 || // A-Z
i === 95 || // _
@@ -183,10 +184,10 @@ define([
}
match =
this.scanIdentifier() ||
this.scanTemplateSequence() ||
this.scanPunctuator() ||
this.scanNumericLiteral();
this.scanNumericLiteral() ||
this.scanIdentifier() ||
this.scanTemplateSequence();
if (match) {
this.skip(match.value.length);
@@ -402,7 +403,6 @@ define([
}
// Numbers must start either with a decimal digit or a point.
if (char !== "." && !isDecimalDigit(char)) {
return null;
}

View File

@@ -66,7 +66,7 @@ define([
return curly;
}
if (this.match('identifier')) {
if (this.match('identifier') || this.match('number')) {
return {
type: 'segment',
value: this.consumeToken().value
@@ -97,7 +97,7 @@ define([
},
metricExpression: function() {
if (!this.match('templateStart') && !this.match('identifier')) {
if (!this.match('templateStart') && !this.match('identifier') && !this.match('number')) {
return null;
}
@@ -153,8 +153,8 @@ define([
var param =
this.functionCall() ||
this.metricExpression() ||
this.numericLiteral() ||
this.metricExpression() ||
this.stringLiteral();
if (!this.match(',')) {

View File

@@ -9,17 +9,29 @@ function (Settings) {
return new Settings({
/**
* elasticsearch url:
* For Basic authentication use: http://username:password@domain.com:9200
*/
elasticsearch: "http://"+window.location.hostname+":9200",
/**
* graphite-web url:
* For Basic authentication use: http://username:password@domain.com
* Basic authentication requires special nginx or apache2 headers for cross origin comain to work
* Basic authentication requires special HTTP headers to be configured
* in nginx or apache for cross origin domain sharing to work (CORS).
* Check install documentation on github
*/
graphiteUrl: "http://"+window.location.hostname+":8080",
default_route: '/dashboard/file/default.json',
/**
* If your graphite server has another timezone than you & users browsers specify the offset here
* Example: "-0500" (for UTC - 5 hours)
*/
timezoneOffset: null,
grafana_index: "grafana-dash",
panel_names: [

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -31,21 +31,25 @@ define([
expect(tokens[6].value).to.be('second');
});
it('should tokenize functions and args', function() {
var lexer = new Lexer("sum(metric.test, 12, 'test')");
it('should tokenize metric expression with number segments', function() {
var lexer = new Lexer("metric.10.12_10.test");
var tokens = lexer.tokenize();
expect(tokens[0].value).to.be('sum');
expect(tokens[0].type).to.be('identifier');
expect(tokens[1].value).to.be('(');
expect(tokens[1].type).to.be('(');
expect(tokens[5].type).to.be(',');
expect(tokens[5].value).to.be(',');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('10');
expect(tokens[4].value).to.be('12_10');
expect(tokens[4].type).to.be('identifier');
});
it('should tokenize func call with numbered metric and number arg', function() {
var lexer = new Lexer("scale(metric.10, 15)");
var tokens = lexer.tokenize();
expect(tokens[0].type).to.be('identifier');
expect(tokens[2].type).to.be('identifier');
expect(tokens[2].value).to.be('metric');
expect(tokens[4].value).to.be('10');
expect(tokens[4].type).to.be('number');
expect(tokens[6].type).to.be('number');
expect(tokens[6].value).to.be('12');
expect(tokens[8].type).to.be('string');
expect(tokens[8].value).to.be('test');
expect(tokens[tokens.length - 1].value).to.be(')');
});
it('should tokenize metric with template parameter', function() {

View File

@@ -13,6 +13,17 @@ define([
expect(rootNode.segments[0].value).to.be('metric');
});
it('simple metric expression with numbers in segments', function() {
var parser = new Parser('metric.10.15_20.5');
var rootNode = parser.getAst();
expect(rootNode.type).to.be('metric');
expect(rootNode.segments.length).to.be(4);
expect(rootNode.segments[1].value).to.be('10');
expect(rootNode.segments[2].value).to.be('15_20');
expect(rootNode.segments[3].value).to.be('5');
});
it('simple metric expression with curly braces', function() {
var parser = new Parser('metric.se1-{count, max}');
var rootNode = parser.getAst();

View File

@@ -12,10 +12,19 @@
.grafana-search-panel {
padding: 6px 10px;
input {
width: 100%;
.box-sizing(border-box);
padding: 15px;
.search-field-wrapper {
input {
width: 100%;
}
button {
margin: 0 2px 0 0;
}
> span {
display: block;
overflow: hidden;
padding-right: 25px;
}
}
.selected td, tr.selected:nth-child(odd)>td {

View File

@@ -103,7 +103,7 @@ code, pre {
.panel div.panel-extra {
font-size: 0.9em;
margin-bottom: 10px;
margin-bottom: 0px;
}
.panel div.panel-extra .extra {
@@ -195,7 +195,7 @@ form input.ng-invalid {
}
.kibana-row {
margin-bottom: 10px;
margin-bottom: 5px;
}
.row-tab {

View File

@@ -13,7 +13,7 @@ be injected into your angular controllers.
angular.module('elasticjs.service', [])
.factory('ejsResource', ['$http', function ($http) {
return function (config) {
return function (config, basicAuth) {
var
@@ -43,6 +43,12 @@ angular.module('elasticjs.service', [])
config.server = '';
}
// set authentication header
if (basicAuth || config.basicAuth) {
config.headers = angular.extend( config.headers||{}, {
"Authorization": "Basic " + (basicAuth||config.basicAuth)
});
}
/* implement the elastic.js client interface for angular */
ejs.client = {
server: function (s) {

1194
src/vendor/moment.js vendored

File diff suppressed because it is too large Load Diff