diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8215e325a1f..6d6c63ee023 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,66 @@
-# 1.8.0 (unreleased)
-
-**New features and improvements**
-
-- [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
-- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
+# 1.8.0 (2014-09-12)
**Fixes**
-- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
+- [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource
+
+# 1.8.0-RC1 (2014-09-12)
+
+**UI polish / changes**
+- [Issue #725](https://github.com/grafana/grafana/issues/725). UI: All modal editors are removed and replaced by an edit pane under menu. The look of editors is also updated and polished. Search dropdown is also shown as pane under menu and has seen some UI polish.
+
+**Filtering/Templating feature overhaul**
+- Filtering renamed to Templating, and filter items to variables
+- Filter editing has gotten its own edit pane with much improved UI and options
+- [Issue #296](https://github.com/grafana/grafana/issues/296). Templating: Can now retrieve variable values from a non-default data source
+- [Issue #219](https://github.com/grafana/grafana/issues/219). Templating: Template variable value selection is now a typeahead autocomplete dropdown
+- [Issue #760](https://github.com/grafana/grafana/issues/760). Templating: Extend template variable syntax to include $variable syntax replacement
+- [Issue #234](https://github.com/grafana/grafana/issues/234). Templating: Interval variable type for time intervals summarize/group by parameter, included "auto" option, and auto step counts option.
+- [Issue #262](https://github.com/grafana/grafana/issues/262). Templating: Ability to use template variables for function parameters via custom variable type, can be used as parameter for movingAverage or scaleToSeconds for example
+- [Issue #312](https://github.com/grafana/grafana/issues/312). Templating: Can now use template variables in panel titles
+- [Issue #613](https://github.com/grafana/grafana/issues/613). Templating: Full support for InfluxDB, filter by part of series names, extract series substrings, nested queries, multipe where clauses!
+- Template variables can be initialized from url, with var-my_varname=value, breaking change, before it was just my_varname.
+- Templating and url state sync has some issues that are not solved for this release, see [Issue #772](https://github.com/grafana/grafana/issues/772) for more details.
+
+**InfluxDB Breaking changes**
+- To better support templating, fill(0) and group by time low limit some changes has been made to the editor and query model schema
+- Currently some of these changes are breaking
+- If you used custom condition filter you need to open the graph in edit mode, the editor will update the schema, and the queries should work again
+- If you used a raw query you need to remove the time filter and replace it with $timeFilter (this is done automatically when you switch from query editor to raw query, but old raw queries needs to updated)
+- If you used group by and later removed the group by the graph could break, open in editor and should correct it
+- InfluxDB annotation queries that used [[timeFilter]] should be updated to use $timeFilter syntax instead
+- Might write an upgrade tool to update dashboards automatically, but right now master (1.8) includes the above breaking changes
+
+**InfluxDB query editor enhancements**
+- [Issue #756](https://github.com/grafana/grafana/issues/756). InfluxDB: Add option for fill(0) and fill(null), integrated help in editor for why this option is important when stacking series
+- [Issue #743](https://github.com/grafana/grafana/issues/743). InfluxDB: A group by time option for all queries in graph panel that supports a low limit for auto group by time, very important for stacking and fill(0)
+- The above to enhancements solves the problems associated with stacked bars and lines when points are missing, these issues are solved:
+- [Issue #673](https://github.com/grafana/grafana/issues/673). InfluxDB: stacked bars missing intermediate data points, unless lines also enabled
+- [Issue #674](https://github.com/grafana/grafana/issues/674). InfluxDB: stacked chart ignoring series without latest values
+- [Issue #534](https://github.com/grafana/grafana/issues/534). InfluxDB: No order in stacked bars mode
+
+**New features and improvements**
+- [Issue #117](https://github.com/grafana/grafana/issues/117). Graphite: Graphite query builder can now handle functions that multiple series as arguments!
+- [Issue #281](https://github.com/grafana/grafana/issues/281). Graphite: Metric node/segment selection is now a textbox with autocomplete dropdown, allow for custom glob expression for single node segment without entering text editor mode.
+- [Issue #304](https://github.com/grafana/grafana/issues/304). Dashboard: View dashboard json, edit/update any panel using json editor, makes it possible to quickly copy a graph from one dashboard to another.
+- [Issue #578](https://github.com/grafana/grafana/issues/578). Dashboard: Row option to display row title even when the row is visible
+- [Issue #672](https://github.com/grafana/grafana/issues/672). Dashboard: panel fullscreen & edit state is present in url, can now link to graph in edit & fullscreen mode.
+- [Issue #709](https://github.com/grafana/grafana/issues/709). Dashboard: Small UI look polish to search results, made dashboard title link are larger
+- [Issue #425](https://github.com/grafana/grafana/issues/425). Graph: New section in 'Display Styles' tab to override any display setting on per series bases (mix and match lines, bars, points, fill, stack, line width etc)
+- [Issue #634](https://github.com/grafana/grafana/issues/634). Dashboard: Dashboard tags now in different colors (from fixed palette) determined by tag name.
+- [Issue #685](https://github.com/grafana/grafana/issues/685). Dashboard: New config.js option to change/remove window title prefix.
+- [Issue #781](https://github.com/grafana/grafana/issues/781). Dashboard: Title URL is now slugified for greater URL readability, works with both ES & InfluxDB storage, is backward compatible
+- [Issue #785](https://github.com/grafana/grafana/issues/785). Elasticsearch: Support for full elasticsearch lucene search grammar when searching for dashboards, better async search
+- [Issue #787](https://github.com/grafana/grafana/issues/787). Dashboard: time range can now be read from URL parameters, will override dashboard saved time range
+
+**Fixes**
+- [Issue #696](https://github.com/grafana/grafana/issues/696). Graph: Fix for y-axis format 'none' when values are in scientific notation (ex 2.3e-13)
+- [Issue #733](https://github.com/grafana/grafana/issues/733). Graph: Fix for tooltip current value decimal precision when 'none' axis format was selected
+- [Issue #697](https://github.com/grafana/grafana/issues/697). Graphite: Fix for Glob syntax in graphite queries ([1-9] and ?) that made the query editor / parser bail and fallback to a text box.
+- [Issue #702](https://github.com/grafana/grafana/issues/702). Graphite: Fix for nonNegativeDerivative function, now possible to not include optional first parameter maxValue
+- [Issue #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected.
+- [Issue #699](https://github.com/grafana/grafana/issues/699). Dashboard: Fix for bug when adding rows from dashboard settings dialog.
+- [Issue #723](https://github.com/grafana/grafana/issues/723). Dashboard: Fix for hide controls setting not used/initialized on dashboard load
+- [Issue #724](https://github.com/grafana/grafana/issues/724). Dashboard: Fix for zoom out causing right hand "to" range to be set in the future.
**Tech**
- Upgraded from angularjs 1.1.5 to 1.3 beta 17;
@@ -18,7 +72,7 @@
# 1.7.1 (unreleased)
**Fixes**
-- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
+- [Issue #691](https://github.com/grafana/grafana/issues/691). Dashboard: Tooltip fixes, sometimes they would not show, and sometimes they would get stuck.
- [Issue #695](https://github.com/grafana/grafana/issues/695). Dashboard: Tooltip on goto home menu icon would get stuck after clicking on it
# 1.7.0 (2014-08-11)
diff --git a/README.md b/README.md
index dbbb566cf90..fd4fef2f364 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ Graphite, InfluxDB & OpenTSDB.
- Import dashboard from Graphite
- Templating
- [Scripted dashboards](http://grafana.org/docs/features/scripted_dashboards)
-- [Dashboard playlists](http://grafana.org/docs/docs/features/playlist)
+- [Dashboard playlists](http://grafana.org/docs/features/playlist)
- [Time range controls](http://grafana.org/docs/features/time_range)
### InfluxDB
diff --git a/latest.json b/latest.json
index 38e862a643a..c66de1bc179 100644
--- a/latest.json
+++ b/latest.json
@@ -1,4 +1,4 @@
{
- "version": "1.7.0",
- "url": "http://grafanarel.s3.amazonaws.com/grafana-1.7.0"
+ "version": "1.8.0-rc1",
+ "url": "http://grafanarel.s3.amazonaws.com/grafana-1.8.0-rc1"
}
diff --git a/package.json b/package.json
index 31c552fe6c2..f84ca9cf8c6 100644
--- a/package.json
+++ b/package.json
@@ -4,7 +4,7 @@
"company": "Coding Instinct AB"
},
"name": "grafana",
- "version": "1.7.0",
+ "version": "1.8.0-rc1",
"repository": {
"type": "git",
"url": "http://github.com/torkelo/grafana.git"
@@ -34,7 +34,7 @@
"grunt-string-replace": "~0.2.4",
"grunt-usemin": "^2.1.1",
"jshint-stylish": "~0.1.5",
- "karma": "~0.12.16",
+ "karma": "~0.12.21",
"karma-chrome-launcher": "~0.1.4",
"karma-coffee-preprocessor": "~0.1.2",
"karma-coverage": "^0.2.5",
diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js
index 7f280cc99d6..cefcb42d289 100644
--- a/src/app/components/kbn.js
+++ b/src/app/components/kbn.js
@@ -1,28 +1,13 @@
-define(['jquery','lodash','moment'],
+define([
+ 'jquery',
+ 'lodash',
+ 'moment'
+],
function($, _, moment) {
'use strict';
var kbn = {};
- /**
- * Calculate a graph interval
- *
- * from:: Date object containing the start time
- * to:: Date object containing the finish time
- * size:: Calculate to approximately this many bars
- * user_interval:: User specified histogram interval
- *
- */
- kbn.calculate_interval = function(from,to,size,user_interval) {
- if(_.isObject(from)) {
- from = from.valueOf();
- }
- if(_.isObject(to)) {
- to = to.valueOf();
- }
- return user_interval === 0 ? kbn.round_interval((to - from)/size) : user_interval;
- };
-
kbn.round_interval = function(interval) {
switch (true) {
// 0.5s
@@ -127,6 +112,28 @@ function($, _, moment) {
s: 1
};
+ kbn.calculateInterval = function(range, resolution, userInterval) {
+ var lowLimitMs = 1; // 1 millisecond default low limit
+ var intervalMs, lowLimitInterval;
+
+ if (userInterval) {
+ if (userInterval[0] === '>') {
+ lowLimitInterval = userInterval.slice(1);
+ lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
+ }
+ else {
+ return userInterval;
+ }
+ }
+
+ intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
+ if (lowLimitMs > intervalMs) {
+ intervalMs = lowLimitMs;
+ }
+
+ return kbn.secondsToHms(intervalMs / 1000);
+ };
+
kbn.describe_interval = function (string) {
var matches = string.match(kbn.interval_regex);
if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
@@ -227,36 +234,36 @@ function($, _, moment) {
if (type === 0) {
roundUp ? dateTime.endOf('year') : dateTime.startOf('year');
} else if (type === 1) {
- dateTime.add('years',num);
+ dateTime.add(num, 'years');
} else if (type === 2) {
- dateTime.subtract('years',num);
+ dateTime.subtract(num, 'years');
}
break;
case 'M':
if (type === 0) {
roundUp ? dateTime.endOf('month') : dateTime.startOf('month');
} else if (type === 1) {
- dateTime.add('months',num);
+ dateTime.add(num, 'months');
} else if (type === 2) {
- dateTime.subtract('months',num);
+ dateTime.subtract(num, 'months');
}
break;
case 'w':
if (type === 0) {
roundUp ? dateTime.endOf('week') : dateTime.startOf('week');
} else if (type === 1) {
- dateTime.add('weeks',num);
+ dateTime.add(num, 'weeks');
} else if (type === 2) {
- dateTime.subtract('weeks',num);
+ dateTime.subtract(num, 'weeks');
}
break;
case 'd':
if (type === 0) {
roundUp ? dateTime.endOf('day') : dateTime.startOf('day');
} else if (type === 1) {
- dateTime.add('days',num);
+ dateTime.add(num, 'days');
} else if (type === 2) {
- dateTime.subtract('days',num);
+ dateTime.subtract(num, 'days');
}
break;
case 'h':
@@ -264,27 +271,27 @@ function($, _, moment) {
if (type === 0) {
roundUp ? dateTime.endOf('hour') : dateTime.startOf('hour');
} else if (type === 1) {
- dateTime.add('hours',num);
+ dateTime.add(num, 'hours');
} else if (type === 2) {
- dateTime.subtract('hours',num);
+ dateTime.subtract(num,'hours');
}
break;
case 'm':
if (type === 0) {
roundUp ? dateTime.endOf('minute') : dateTime.startOf('minute');
} else if (type === 1) {
- dateTime.add('minutes',num);
+ dateTime.add(num, 'minutes');
} else if (type === 2) {
- dateTime.subtract('minutes',num);
+ dateTime.subtract(num, 'minutes');
}
break;
case 's':
if (type === 0) {
roundUp ? dateTime.endOf('second') : dateTime.startOf('second');
} else if (type === 1) {
- dateTime.add('seconds',num);
+ dateTime.add(num, 'seconds');
} else if (type === 2) {
- dateTime.subtract('seconds',num);
+ dateTime.subtract(num, 'seconds');
}
break;
default:
@@ -536,7 +543,7 @@ function($, _, moment) {
var formatted = String(Math.round(value * factor) / factor);
// if exponent return directly
- if (formatted.indexOf('e') !== -1) {
+ if (formatted.indexOf('e') !== -1 || value === 0) {
return formatted;
}
@@ -648,5 +655,21 @@ function($, _, moment) {
}
};
+ kbn.slugifyForUrl = function(str) {
+ return str
+ .toLowerCase()
+ .replace(/[^\w ]+/g,'')
+ .replace(/ +/g,'-');
+ };
+
+ kbn.stringToJsRegex = function(str) {
+ if (str[0] !== '/') {
+ return new RegExp(str);
+ }
+
+ var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+ return new RegExp(match[1], match[2]);
+ };
+
return kbn;
});
diff --git a/src/app/components/settings.js b/src/app/components/settings.js
index 3edafc5a407..6afba222b4b 100644
--- a/src/app/components/settings.js
+++ b/src/app/components/settings.js
@@ -14,12 +14,13 @@ function (_, crypto) {
*/
var defaults = {
datasources : {},
+ window_title_prefix : 'Grafana - ',
panels : ['graph', 'text'],
plugins : {},
default_route : '/dashboard/file/default.json',
playlist_timespan : "1m",
unsaved_changes_warning : true,
- search : { max_results: 20 },
+ search : { max_results: 16 },
admin : {}
};
diff --git a/src/app/panels/graph/timeSeries.js b/src/app/components/timeSeries.js
similarity index 52%
rename from src/app/panels/graph/timeSeries.js
rename to src/app/components/timeSeries.js
index f1794cd6a30..4c58c211cc3 100644
--- a/src/app/panels/graph/timeSeries.js
+++ b/src/app/components/timeSeries.js
@@ -5,15 +5,56 @@ define([
function (_, kbn) {
'use strict';
- var ts = {};
-
- ts.ZeroFilled = function (opts) {
+ function TimeSeries(opts) {
this.datapoints = opts.datapoints;
this.info = opts.info;
this.label = opts.info.alias;
+ }
+
+ function matchSeriesOverride(aliasOrRegex, seriesAlias) {
+ if (!aliasOrRegex) { return false; }
+
+ if (aliasOrRegex[0] === '/') {
+ var regex = kbn.stringToJsRegex(aliasOrRegex);
+ return seriesAlias.match(regex) != null;
+ }
+
+ return aliasOrRegex === seriesAlias;
+ }
+
+ function translateFillOption(fill) {
+ return fill === 0 ? 0.001 : fill/10;
+ }
+
+ TimeSeries.prototype.applySeriesOverrides = function(overrides) {
+ this.lines = {};
+ this.points = {};
+ this.bars = {};
+ this.info.yaxis = 1;
+ this.zindex = 0;
+ delete this.stack;
+
+ for (var i = 0; i < overrides.length; i++) {
+ var override = overrides[i];
+ if (!matchSeriesOverride(override.alias, this.info.alias)) {
+ continue;
+ }
+ if (override.lines !== void 0) { this.lines.show = override.lines; }
+ if (override.points !== void 0) { this.points.show = override.points; }
+ if (override.bars !== void 0) { this.bars.show = override.bars; }
+ if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); }
+ if (override.stack !== void 0) { this.stack = override.stack; }
+ if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; }
+ if (override.pointradius !== void 0) { this.points.radius = override.pointradius; }
+ if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; }
+ if (override.zindex !== void 0) { this.zindex = override.zindex; }
+ if (override.yaxis !== void 0) {
+ this.info.yaxis = override.yaxis;
+ }
+ }
};
- ts.ZeroFilled.prototype.getFlotPairs = function (fillStyle, yFormats) {
+ TimeSeries.prototype.getFlotPairs = function (fillStyle, yFormats) {
var result = [];
this.color = this.info.color;
@@ -74,5 +115,6 @@ function (_, kbn) {
return result;
};
- return ts;
-});
\ No newline at end of file
+ return TimeSeries;
+
+});
diff --git a/src/app/controllers/all.js b/src/app/controllers/all.js
index 1631dc96197..4ce2a6f5822 100644
--- a/src/app/controllers/all.js
+++ b/src/app/controllers/all.js
@@ -13,5 +13,7 @@ define([
'./playlistCtrl',
'./inspectCtrl',
'./opentsdbTargetCtrl',
- './console-ctrl',
+ './annotationsEditorCtrl',
+ './templateEditorCtrl',
+ './jsonEditorCtrl',
], function () {});
diff --git a/src/app/panels/annotations/editor.js b/src/app/controllers/annotationsEditorCtrl.js
similarity index 51%
rename from src/app/panels/annotations/editor.js
rename to src/app/controllers/annotationsEditorCtrl.js
index 535857418fa..9b6da497dce 100644
--- a/src/app/panels/annotations/editor.js
+++ b/src/app/controllers/annotationsEditorCtrl.js
@@ -1,19 +1,14 @@
-/*
-
-*/
define([
'angular',
- 'app',
- 'lodash'
+ 'lodash',
+ 'jquery'
],
-function (angular, app, _) {
+function (angular, _, $) {
'use strict';
- var module = angular.module('grafana.panels.annotations', []);
- app.useModule(module);
+ var module = angular.module('grafana.controllers');
module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
-
var annotationDefaults = {
name: '',
datasource: null,
@@ -25,39 +20,57 @@ function (angular, app, _) {
};
$scope.init = function() {
- $scope.currentAnnotation = angular.copy(annotationDefaults);
- $scope.currentIsNew = true;
+ $scope.editor = { index: 0 };
$scope.datasources = datasourceSrv.getAnnotationSources();
+ $scope.annotations = $scope.dashboard.annotations.list;
+ $scope.reset();
- if ($scope.datasources.length > 0) {
- $scope.currentDatasource = $scope.datasources[0];
- }
+ $scope.$watch('editor.index', function(newVal) {
+ if (newVal !== 2) {
+ $scope.reset();
+ }
+ });
};
- $scope.setDatasource = function() {
- $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
- };
-
- $scope.edit = function(annotation) {
- $scope.currentAnnotation = annotation;
- $scope.currentIsNew = false;
- $scope.currentDatasource = _.findWhere($scope.datasources, { name: annotation.datasource });
-
+ $scope.datasourceChanged = function() {
+ $scope.currentDatasource = _.findWhere($scope.datasources, { name: $scope.currentAnnotation.datasource });
if (!$scope.currentDatasource) {
$scope.currentDatasource = $scope.datasources[0];
}
};
- $scope.update = function() {
+ $scope.edit = function(annotation) {
+ $scope.currentAnnotation = annotation;
+ $scope.currentIsNew = false;
+ $scope.datasourceChanged();
+
+ $scope.editor.index = 2;
+ $(".tooltip.in").remove();
+ };
+
+ $scope.reset = function() {
$scope.currentAnnotation = angular.copy(annotationDefaults);
$scope.currentIsNew = true;
+ $scope.datasourceChanged();
+ $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
+ };
+
+ $scope.update = function() {
+ $scope.reset();
+ $scope.editor.index = 0;
};
$scope.add = function() {
- $scope.currentAnnotation.datasource = $scope.currentDatasource.name;
- $scope.panel.annotations.push($scope.currentAnnotation);
- $scope.currentAnnotation = angular.copy(annotationDefaults);
+ $scope.annotations.push($scope.currentAnnotation);
+ $scope.reset();
+ $scope.editor.index = 0;
+ };
+
+ $scope.removeAnnotation = function(annotation) {
+ var index = _.indexOf($scope.annotations, annotation);
+ $scope.annotations.splice(index, 1);
};
});
+
});
diff --git a/src/app/controllers/dashboardCtrl.js b/src/app/controllers/dashboardCtrl.js
index 5620b7305a2..424f0e225e8 100644
--- a/src/app/controllers/dashboardCtrl.js
+++ b/src/app/controllers/dashboardCtrl.js
@@ -11,53 +11,62 @@ function (angular, $, config, _) {
var module = angular.module('grafana.controllers');
module.controller('DashboardCtrl', function(
- $scope, $rootScope, dashboardKeybindings,
- filterSrv, dashboardSrv, dashboardViewStateSrv,
- panelMoveSrv, timer) {
+ $scope,
+ $rootScope,
+ dashboardKeybindings,
+ timeSrv,
+ templateValuesSrv,
+ dashboardSrv,
+ dashboardViewStateSrv,
+ panelMoveSrv,
+ timer,
+ $timeout) {
$scope.editor = { index: 0 };
$scope.panelNames = config.panels;
+ var resizeEventTimeout;
$scope.init = function() {
$scope.availablePanels = config.panels;
$scope.onAppEvent('setup-dashboard', $scope.setupDashboard);
+ $scope.onAppEvent('show-json-editor', $scope.showJsonEditor);
+ $scope.reset_row();
+ $scope.registerWindowResizeEvent();
+ };
+
+ $scope.registerWindowResizeEvent = function() {
+ angular.element(window).bind('resize', function() {
+ $timeout.cancel(resizeEventTimeout);
+ resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200);
+ });
};
$scope.setupDashboard = function(event, dashboardData) {
- timer.cancel_all();
-
$rootScope.performance.dashboardLoadStart = new Date().getTime();
$rootScope.performance.panelsInitialized = 0;
- $rootScope.performance.panelsRendered= 0;
+ $rootScope.performance.panelsRendered = 0;
$scope.dashboard = dashboardSrv.create(dashboardData);
$scope.dashboardViewState = dashboardViewStateSrv.create($scope);
- $scope.grafana.style = $scope.dashboard.style;
-
- $scope.filter = filterSrv;
- $scope.filter.init($scope.dashboard);
-
- var panelMove = panelMoveSrv.create($scope.dashboard);
-
- $scope.panelMoveDrop = panelMove.onDrop;
- $scope.panelMoveStart = panelMove.onStart;
- $scope.panelMoveStop = panelMove.onStop;
- $scope.panelMoveOver = panelMove.onOver;
- $scope.panelMoveOut = panelMove.onOut;
-
- window.document.title = 'Grafana - ' + $scope.dashboard.title;
-
- // start auto refresh
- if($scope.dashboard.refresh) {
- $scope.dashboard.set_interval($scope.dashboard.refresh);
- }
+ // init services
+ timeSrv.init($scope.dashboard);
+ templateValuesSrv.init($scope.dashboard, $scope.dashboardViewState);
+ panelMoveSrv.init($scope.dashboard, $scope);
+ $scope.checkFeatureToggles();
dashboardKeybindings.shortcuts($scope);
+ $scope.setWindowTitleAndTheme();
+
$scope.emitAppEvent("dashboard-loaded", $scope.dashboard);
};
+ $scope.setWindowTitleAndTheme = function() {
+ window.document.title = config.window_title_prefix + $scope.dashboard.title;
+ $scope.grafana.style = $scope.dashboard.style;
+ };
+
$scope.isPanel = function(obj) {
if(!_.isNull(obj) && !_.isUndefined(obj) && !_.isUndefined(obj.type)) {
return true;
@@ -84,6 +93,15 @@ function (angular, $, config, _) {
};
};
+ $scope.edit_path = function(type) {
+ var p = $scope.panel_path(type);
+ if(p) {
+ return p+'/editor.html';
+ } else {
+ return false;
+ }
+ };
+
$scope.panel_path =function(type) {
if(type) {
return 'app/panels/'+type.replace(".","/");
@@ -92,13 +110,15 @@ function (angular, $, config, _) {
}
};
- $scope.edit_path = function(type) {
- var p = $scope.panel_path(type);
- if(p) {
- return p+'/editor.html';
- } else {
- return false;
- }
+ $scope.showJsonEditor = function(evt, options) {
+ var editScope = $rootScope.$new();
+ editScope.object = options.object;
+ editScope.updateHandler = options.updateHandler;
+ $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/edit_json.html', scope: editScope });
+ };
+
+ $scope.checkFeatureToggles = function() {
+ $scope.submenuEnabled = $scope.dashboard.templating.enable || $scope.dashboard.annotations.enable;
};
$scope.setEditorTabs = function(panelMeta) {
diff --git a/src/app/controllers/dashboardNavCtrl.js b/src/app/controllers/dashboardNavCtrl.js
index 7fe6e03f78b..b6813b7ce19 100644
--- a/src/app/controllers/dashboardNavCtrl.js
+++ b/src/app/controllers/dashboardNavCtrl.js
@@ -11,14 +11,13 @@ function (angular, _, moment, config, store) {
var module = angular.module('grafana.controllers');
- module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv) {
+ module.controller('DashboardNavCtrl', function($scope, $rootScope, alertSrv, $location, playlistSrv, datasourceSrv, timeSrv) {
$scope.init = function() {
$scope.db = datasourceSrv.getGrafanaDB();
- $scope.onAppEvent('save-dashboard', function() {
- $scope.saveDashboard();
- });
+ $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
+ $scope.onAppEvent('delete-dashboard', $scope.deleteDashboard);
$scope.onAppEvent('zoom-out', function() {
$scope.zoom(2);
@@ -57,10 +56,10 @@ function (angular, _, moment, config, store) {
$scope.isAdmin = function() {
if (!config.admin || !config.admin.password) { return true; }
- if (this.passwordCache() === config.admin.password) { return true; }
+ if ($scope.passwordCache() === config.admin.password) { return true; }
var password = window.prompt("Admin password", "");
- this.passwordCache(password);
+ $scope.passwordCache(password);
if (password === config.admin.password) { return true; }
@@ -69,16 +68,22 @@ function (angular, _, moment, config, store) {
return false;
};
+ $scope.openSearch = function() {
+ $scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' });
+ };
+
$scope.saveDashboard = function() {
- if (!this.isAdmin()) { return false; }
+ if (!$scope.isAdmin()) { return false; }
var clone = angular.copy($scope.dashboard);
$scope.db.saveDashboard(clone)
.then(function(result) {
alertSrv.set('Dashboard Saved', 'Dashboard has been saved as "' + result.title + '"','success', 5000);
- $location.search({});
- $location.path(result.url);
+ if (result.url !== $location.path()) {
+ $location.search({});
+ $location.path(result.url);
+ }
$rootScope.$emit('dashboard-saved', $scope.dashboard);
@@ -87,13 +92,14 @@ function (angular, _, moment, config, store) {
});
};
- $scope.deleteDashboard = function(id) {
+ $scope.deleteDashboard = function(evt, options) {
if (!confirm('Are you sure you want to delete dashboard?')) {
return;
}
- if (!this.isAdmin()) { return false; }
+ if (!$scope.isAdmin()) { return false; }
+ var id = options.id;
$scope.db.deleteDashboard(id).then(function(id) {
alertSrv.set('Dashboard Deleted', id + ' has been deleted', 'success', 5000);
}, function() {
@@ -106,26 +112,24 @@ function (angular, _, moment, config, store) {
window.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime());
};
- // function $scope.zoom
- // factor :: Zoom factor, so 0.5 = cuts timespan in half, 2 doubles timespan
$scope.zoom = function(factor) {
- var _range = $scope.filter.timeRange();
- var _timespan = (_range.to.valueOf() - _range.from.valueOf());
- var _center = _range.to.valueOf() - _timespan/2;
+ var range = timeSrv.timeRange();
- var _to = (_center + (_timespan*factor)/2);
- var _from = (_center - (_timespan*factor)/2);
+ var timespan = (range.to.valueOf() - range.from.valueOf());
+ var center = range.to.valueOf() - timespan/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();
+ var to = (center + (timespan*factor)/2);
+ var from = (center - (timespan*factor)/2);
+
+ if(to > Date.now() && range.to <= Date.now()) {
+ var offset = to - Date.now();
+ from = from - offset;
+ to = Date.now();
}
- $scope.filter.setTime({
- from:moment.utc(_from).toDate(),
- to:moment.utc(_to).toDate(),
+ timeSrv.setTime({
+ from: moment.utc(from).toDate(),
+ to: moment.utc(to).toDate(),
});
};
@@ -133,6 +137,10 @@ function (angular, _, moment, config, store) {
$scope.grafana.style = $scope.dashboard.style;
};
+ $scope.editJson = function() {
+ $scope.emitAppEvent('show-json-editor', { object: $scope.dashboard });
+ };
+
$scope.openSaveDropdown = function() {
$scope.isFavorite = playlistSrv.isCurrentFavorite($scope.dashboard);
$scope.saveDropdownOpened = true;
diff --git a/src/app/controllers/graphiteTarget.js b/src/app/controllers/graphiteTarget.js
index 6b74455f59c..27299474bc0 100644
--- a/src/app/controllers/graphiteTarget.js
+++ b/src/app/controllers/graphiteTarget.js
@@ -9,11 +9,13 @@ function (angular, _, config, gfunc, Parser) {
'use strict';
var module = angular.module('grafana.controllers');
+ var targetLetters = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O'];
- module.controller('GraphiteTargetCtrl', function($scope, $sce) {
+ module.controller('GraphiteTargetCtrl', function($scope, $sce, templateSrv) {
$scope.init = function() {
$scope.target.target = $scope.target.target || '';
+ $scope.targetLetters = targetLetters;
parseTarget();
};
@@ -52,6 +54,13 @@ function (angular, _, config, gfunc, Parser) {
checkOtherSegments($scope.segments.length - 1);
}
+ function addFunctionParameter(func, value, index, shiftBack) {
+ if (shiftBack) {
+ index = Math.max(index - 1, 0);
+ }
+ func.params[index] = value;
+ }
+
function parseTargeRecursive(astNode, func, index) {
if (astNode === null) {
return null;
@@ -59,7 +68,7 @@ function (angular, _, config, gfunc, Parser) {
switch(astNode.type) {
case 'function':
- var innerFunc = gfunc.createFuncInstance(astNode.name);
+ var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
_.each(astNode.params, function(param, index) {
parseTargeRecursive(param, innerFunc, index);
@@ -69,24 +78,23 @@ function (angular, _, config, gfunc, Parser) {
$scope.functions.push(innerFunc);
break;
+ case 'series-ref':
+ addFunctionParameter(func, astNode.value, index, $scope.segments.length > 0);
+ break;
case 'string':
case 'number':
if ((index-1) >= func.def.params.length) {
throw { message: 'invalid number of parameters to method ' + func.def.name };
}
-
- if (index === 0) {
- func.params[index] = astNode.value;
- }
- else {
- func.params[index - 1] = astNode.value;
- }
-
+ addFunctionParameter(func, astNode.value, index, true);
break;
-
case 'metric':
if ($scope.segments.length > 0) {
- throw { message: 'Multiple metric params not supported, use text editor.' };
+ if (astNode.segments.length !== 1) {
+ throw { message: 'Multiple metric params not supported, use text editor.' };
+ }
+ addFunctionParameter(func, astNode.segments[0].value, index, true);
+ break;
}
$scope.segments = _.map(astNode.segments, function(segment) {
@@ -110,11 +118,13 @@ function (angular, _, config, gfunc, Parser) {
}
var path = getSegmentPathUpTo(fromIndex + 1);
- return $scope.datasource.metricFindQuery($scope.filter, path)
+ return $scope.datasource.metricFindQuery(path)
.then(function(segments) {
if (segments.length === 0) {
- $scope.segments = $scope.segments.splice(0, fromIndex);
- $scope.segments.push(new MetricSegment('select metric'));
+ if (path !== '') {
+ $scope.segments = $scope.segments.splice(0, fromIndex);
+ $scope.segments.push(new MetricSegment('select metric'));
+ }
return;
}
if (segments[0].expandable) {
@@ -144,19 +154,18 @@ function (angular, _, config, gfunc, Parser) {
$scope.getAltSegments = function (index) {
$scope.altSegments = [];
- var query = index === 0 ?
- '*' : getSegmentPathUpTo(index) + '.*';
+ var query = index === 0 ? '*' : getSegmentPathUpTo(index) + '.*';
- return $scope.datasource.metricFindQuery($scope.filter, query)
+ return $scope.datasource.metricFindQuery(query)
.then(function(segments) {
$scope.altSegments = _.map(segments, function(segment) {
return new MetricSegment({ value: segment.text, expandable: segment.expandable });
});
- _.each($scope.filter.templateParameters, function(templateParameter) {
+ _.each(templateSrv.variables, function(variable) {
$scope.altSegments.unshift(new MetricSegment({
type: 'template',
- value: '[[' + templateParameter.name + ']]',
+ value: '$' + variable.name,
expandable: true,
}));
});
@@ -168,17 +177,14 @@ function (angular, _, config, gfunc, Parser) {
});
};
- $scope.setSegment = function (altIndex, segmentIndex) {
+ $scope.segmentValueChanged = function (segment, segmentIndex) {
delete $scope.parserError;
- $scope.segments[segmentIndex].value = $scope.altSegments[altIndex].value;
- $scope.segments[segmentIndex].html = $scope.altSegments[altIndex].html;
-
if ($scope.functions.length > 0 && $scope.functions[0].def.fake) {
$scope.functions = [];
}
- if ($scope.altSegments[altIndex].expandable) {
+ if (segment.expandable) {
return checkOtherSegments(segmentIndex + 1)
.then(function () {
setSegmentFocus(segmentIndex + 1);
@@ -219,13 +225,17 @@ function (angular, _, config, gfunc, Parser) {
};
$scope.addFunction = function(funcDef) {
- var newFunc = gfunc.createFuncInstance(funcDef);
+ var newFunc = gfunc.createFuncInstance(funcDef, { withDefaultParams: true });
newFunc.added = true;
$scope.functions.push(newFunc);
$scope.moveAliasFuncLast();
$scope.smartlyHandleNewAliasByNode(newFunc);
+ if ($scope.segments.length === 1 && $scope.segments[0].value === 'select metric') {
+ $scope.segments = [];
+ }
+
if (!newFunc.params.length && newFunc.added) {
$scope.targetChanged();
}
@@ -287,13 +297,7 @@ function (angular, _, config, gfunc, Parser) {
this.value = options.value;
this.type = options.type;
this.expandable = options.expandable;
-
- if (options.type === 'template') {
- this.html = $sce.trustAsHtml("" + options.value + " ");
- }
- else {
- this.html = $sce.trustAsHtml(this.value);
- }
+ this.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(this.value));
}
});
diff --git a/src/app/controllers/influxTargetCtrl.js b/src/app/controllers/influxTargetCtrl.js
index bb34df8f608..b8101ab9577 100644
--- a/src/app/controllers/influxTargetCtrl.js
+++ b/src/app/controllers/influxTargetCtrl.js
@@ -11,8 +11,23 @@ function (angular) {
module.controller('InfluxTargetCtrl', function($scope, $timeout) {
$scope.init = function() {
- $scope.target.function = $scope.target.function || 'mean';
- $scope.target.column = $scope.target.column || 'value';
+ var target = $scope.target;
+
+ target.function = target.function || 'mean';
+ target.column = target.column || 'value';
+
+ // backward compatible correction of schema
+ if (target.condition_value) {
+ target.condition = target.condition_key + ' ' + target.condition_op + ' ' + target.condition_value;
+ delete target.condition_key;
+ delete target.condition_op;
+ delete target.condition_value;
+ }
+
+ if (target.groupby_field_add === false) {
+ target.groupby_field = '';
+ delete target.groupby_field_add;
+ }
$scope.rawQuery = false;
@@ -24,7 +39,7 @@ function (angular) {
];
$scope.operators = ['=', '=~', '>', '<', '!~', '<>'];
- $scope.oldSeries = $scope.target.series;
+ $scope.oldSeries = target.series;
$scope.$on('typeahead-updated', function() {
$timeout($scope.get_data);
});
diff --git a/src/app/controllers/jsonEditorCtrl.js b/src/app/controllers/jsonEditorCtrl.js
new file mode 100644
index 00000000000..60bda8514b7
--- /dev/null
+++ b/src/app/controllers/jsonEditorCtrl.js
@@ -0,0 +1,22 @@
+define([
+ 'angular',
+ 'lodash'
+],
+function (angular) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('JsonEditorCtrl', function($scope) {
+
+ $scope.json = angular.toJson($scope.object, true);
+ $scope.canUpdate = $scope.updateHandler !== void 0;
+
+ $scope.update = function () {
+ var newObject = angular.fromJson($scope.json);
+ $scope.updateHandler(newObject, $scope.object);
+ };
+
+ });
+
+});
diff --git a/src/app/controllers/playlistCtrl.js b/src/app/controllers/playlistCtrl.js
index ef605b48b7d..9e6a60013e2 100644
--- a/src/app/controllers/playlistCtrl.js
+++ b/src/app/controllers/playlistCtrl.js
@@ -13,7 +13,6 @@ function (angular, _, config) {
$scope.init = function() {
$scope.timespan = config.playlist_timespan;
$scope.loadFavorites();
- $scope.$on('modal-opened', $scope.loadFavorites);
};
$scope.loadFavorites = function() {
diff --git a/src/app/controllers/row.js b/src/app/controllers/row.js
index 314559405ea..621c3eddda5 100644
--- a/src/app/controllers/row.js
+++ b/src/app/controllers/row.js
@@ -13,7 +13,6 @@ function (angular, app, _) {
title: "Row",
height: "150px",
collapse: false,
- editable: true,
panels: [],
};
@@ -76,6 +75,19 @@ function (angular, app, _) {
}
};
+ $scope.replacePanel = function(newPanel, oldPanel) {
+ var row = $scope.row;
+ var index = _.indexOf(row.panels, oldPanel);
+ row.panels.splice(index, 1);
+
+ // adding it back needs to be done in next digest
+ $timeout(function() {
+ newPanel.id = oldPanel.id;
+ newPanel.span = oldPanel.span;
+ row.panels.splice(index, 0, newPanel);
+ });
+ };
+
$scope.duplicatePanel = function(panel, row) {
$scope.dashboard.duplicatePanel(panel, row || $scope.row);
};
diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js
index fd4e97e836d..98e6616f0a4 100644
--- a/src/app/controllers/search.js
+++ b/src/app/controllers/search.js
@@ -9,7 +9,7 @@ function (angular, _, config, $) {
var module = angular.module('grafana.controllers');
- module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, datasourceSrv) {
+ module.controller('SearchCtrl', function($scope, $rootScope, $element, $location, datasourceSrv, $timeout) {
$scope.init = function() {
$scope.giveSearchFocus = 0;
@@ -17,18 +17,25 @@ function (angular, _, config, $) {
$scope.results = {dashboards: [], tags: [], metrics: []};
$scope.query = { query: 'title:' };
$scope.db = datasourceSrv.getGrafanaDB();
- $scope.onAppEvent('open-search', $scope.openSearch);
+ $scope.currentSearchId = 0;
+
+ $timeout(function() {
+ $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
+ $scope.query.query = 'title:';
+ $scope.search();
+ }, 100);
+
};
$scope.keyDown = function (evt) {
if (evt.keyCode === 27) {
- $element.find('.dropdown-toggle').dropdown('toggle');
+ $scope.emitAppEvent('hide-dash-editor');
}
if (evt.keyCode === 40) {
- $scope.selectedIndex++;
+ $scope.moveSelection(1);
}
if (evt.keyCode === 38) {
- $scope.selectedIndex--;
+ $scope.moveSelection(-1);
}
if (evt.keyCode === 13) {
if ($scope.tagsOnly) {
@@ -50,7 +57,16 @@ function (angular, _, config, $) {
}
};
- $scope.shareDashboard = function(title, id) {
+ $scope.moveSelection = function(direction) {
+ $scope.selectedIndex = Math.max(Math.min($scope.selectedIndex + direction, $scope.resultCount - 1), 0);
+ };
+
+ $scope.goToDashboard = function(id) {
+ $location.path("/dashboard/db/" + id);
+ };
+
+ $scope.shareDashboard = function(title, id, $event) {
+ $event.stopPropagation();
var baseUrl = window.location.href.replace(window.location.hash,'');
$scope.share = {
@@ -60,11 +76,22 @@ function (angular, _, config, $) {
};
$scope.searchDashboards = function(queryString) {
+ // bookeeping for determining stale search requests
+ var searchId = $scope.currentSearchId + 1;
+ $scope.currentSearchId = searchId > $scope.currentSearchId ? searchId : $scope.currentSearchId;
+
return $scope.db.searchDashboards(queryString)
.then(function(results) {
+ // since searches are async, it's possible that these results are not for the latest search. throw
+ // them away if so
+ if (searchId < $scope.currentSearchId) {
+ return;
+ }
+
$scope.tagsOnly = results.tagsOnly;
$scope.results.dashboards = results.dashboards;
$scope.results.tags = results.tags;
+ $scope.resultCount = results.tagsOnly ? results.tags.length : results.dashboards.length;
});
};
@@ -78,8 +105,7 @@ function (angular, _, config, $) {
}
};
- $scope.showTags = function(evt) {
- evt.stopPropagation();
+ $scope.showTags = function() {
$scope.tagsOnly = !$scope.tagsOnly;
$scope.query.query = $scope.tagsOnly ? "tags!:" : "";
$scope.giveSearchFocus = $scope.giveSearchFocus + 1;
@@ -89,20 +115,13 @@ function (angular, _, config, $) {
$scope.search = function() {
$scope.showImport = false;
- $scope.selectedIndex = -1;
-
+ $scope.selectedIndex = 0;
$scope.searchDashboards($scope.query.query);
};
- $scope.openSearch = function (evt) {
- if (evt) {
- $element.next().find('.dropdown-toggle').dropdown('toggle');
- }
-
- $scope.searchOpened = true;
- $scope.giveSearchFocus = $scope.giveSearchFocus + 1;
- $scope.query.query = 'title:';
- $scope.search();
+ $scope.deleteDashboard = function(id, evt) {
+ evt.stopPropagation();
+ $scope.emitAppEvent('delete-dashboard', { id: id });
};
$scope.addMetricToCurrentDashboard = function (metricId) {
@@ -121,8 +140,7 @@ function (angular, _, config, $) {
});
};
- $scope.toggleImport = function ($event) {
- $event.stopPropagation();
+ $scope.toggleImport = function () {
$scope.showImport = !$scope.showImport;
};
@@ -134,16 +152,48 @@ function (angular, _, config, $) {
module.directive('xngFocus', function() {
return function(scope, element, attrs) {
- $(element).click(function(e) {
+ element.click(function(e) {
e.stopPropagation();
});
scope.$watch(attrs.xngFocus,function (newValue) {
+ if (!newValue) {
+ return;
+ }
setTimeout(function() {
- newValue && element.focus();
+ element.focus();
+ var pos = element.val().length * 2;
+ element[0].setSelectionRange(pos, pos);
}, 200);
},true);
};
});
+ module.directive('tagColorFromName', function() {
+
+ function djb2(str) {
+ var hash = 5381;
+ for (var i = 0; i < str.length; i++) {
+ hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
+ }
+ return hash;
+ }
+
+ return function (scope, element) {
+ var name = _.isString(scope.tag) ? scope.tag : scope.tag.term;
+ var hash = djb2(name.toLowerCase());
+ var colors = [
+ "#E24D42","#1F78C1","#BA43A9","#705DA0","#466803",
+ "#508642","#447EBC","#C15C17","#890F02","#757575",
+ "#0A437C","#6D1F62","#584477","#629E51","#2F4F4F",
+ "#BF1B00","#806EB7","#8a2eb8", "#699e00","#000000",
+ "#3F6833","#2F575E","#99440A","#E0752D","#0E4AB4",
+ "#58140C","#052B51","#511749","#3F2B5B",
+ ];
+ var color = colors[Math.abs(hash % colors.length)];
+ element.css("background-color", color);
+ };
+
+ });
+
});
diff --git a/src/app/controllers/submenuCtrl.js b/src/app/controllers/submenuCtrl.js
index b75fab56043..a1067ee70ac 100644
--- a/src/app/controllers/submenuCtrl.js
+++ b/src/app/controllers/submenuCtrl.js
@@ -8,7 +8,7 @@ function (angular, app, _) {
var module = angular.module('grafana.controllers');
- module.controller('SubmenuCtrl', function($scope) {
+ module.controller('SubmenuCtrl', function($scope, $q, $rootScope, templateValuesSrv) {
var _d = {
enable: true
};
@@ -18,10 +18,20 @@ function (angular, app, _) {
$scope.init = function() {
$scope.panel = $scope.pulldown;
$scope.row = $scope.pulldown;
+ $scope.variables = $scope.dashboard.templating.list;
+ };
+
+ $scope.disableAnnotation = function (annotation) {
+ annotation.enable = !annotation.enable;
+ $rootScope.$broadcast('refresh');
+ };
+
+ $scope.setVariableValue = function(param, option) {
+ templateValuesSrv.setVariableValue(param, option);
};
$scope.init();
});
-});
\ No newline at end of file
+});
diff --git a/src/app/controllers/templateEditorCtrl.js b/src/app/controllers/templateEditorCtrl.js
new file mode 100644
index 00000000000..058a190658b
--- /dev/null
+++ b/src/app/controllers/templateEditorCtrl.js
@@ -0,0 +1,84 @@
+define([
+ 'angular',
+ 'lodash',
+],
+function (angular, _) {
+ 'use strict';
+
+ var module = angular.module('grafana.controllers');
+
+ module.controller('TemplateEditorCtrl', function($scope, datasourceSrv, templateSrv, templateValuesSrv, alertSrv) {
+
+ var replacementDefaults = {
+ type: 'query',
+ datasource: null,
+ refresh_on_load: false,
+ name: '',
+ options: [],
+ includeAll: false,
+ allFormat: 'glob',
+ };
+
+ $scope.init = function() {
+ $scope.editor = { index: 0 };
+ $scope.datasources = datasourceSrv.getMetricSources();
+ $scope.variables = templateSrv.variables;
+ $scope.reset();
+
+ $scope.$watch('editor.index', function(index) {
+ if ($scope.currentIsNew === false && index === 1) {
+ $scope.reset();
+ }
+ });
+ };
+
+ $scope.add = function() {
+ $scope.variables.push($scope.current);
+ $scope.update();
+ };
+
+ $scope.runQuery = function() {
+ return templateValuesSrv.updateOptions($scope.current).then(function() {
+ }, function(err) {
+ alertSrv.set('Templating', 'Failed to run query for variable values: ' + err.message, 'error');
+ });
+ };
+
+ $scope.edit = function(variable) {
+ $scope.current = variable;
+ $scope.currentIsNew = false;
+ $scope.editor.index = 2;
+
+ if ($scope.current.datasource === void 0) {
+ $scope.current.datasource = null;
+ $scope.current.type = 'query';
+ $scope.current.allFormat = 'Glob';
+ }
+ };
+
+ $scope.update = function() {
+ $scope.runQuery().then(function() {
+ $scope.reset();
+ $scope.editor.index = 0;
+ });
+ };
+
+ $scope.reset = function() {
+ $scope.currentIsNew = true;
+ $scope.current = angular.copy(replacementDefaults);
+ };
+
+ $scope.typeChanged = function () {
+ if ($scope.current.type === 'interval') {
+ $scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
+ }
+ };
+
+ $scope.removeVariable = function(variable) {
+ var index = _.indexOf($scope.variables, variable);
+ $scope.variables.splice(index, 1);
+ };
+
+ });
+
+});
diff --git a/src/app/dashboards/default.json b/src/app/dashboards/default.json
index 15ec5cf1dd7..ab22805f99c 100644
--- a/src/app/dashboards/default.json
+++ b/src/app/dashboards/default.json
@@ -8,12 +8,11 @@
{
"title": "New row",
"height": "150px",
- "editable": true,
"collapse": false,
- "collapsable": true,
+ "editable": true,
"panels": [
{
- "error": false,
+ "id": 1,
"span": 12,
"editable": true,
"type": "text",
@@ -22,50 +21,43 @@
"style": {},
"title": "Welcome to"
}
- ],
- "notice": false
+ ]
},
{
"title": "Welcome to Grafana",
"height": "210px",
- "editable": true,
"collapse": false,
- "collapsable": true,
+ "editable": true,
"panels": [
{
- "error": false,
+ "id": 2,
"span": 6,
- "editable": true,
"type": "text",
- "loadingEditor": false,
"mode": "html",
"content": " \n\n
",
"style": {},
"title": "Documentation Links"
},
{
- "error": false,
+ "id": 3,
"span": 6,
- "editable": true,
"type": "text",
"mode": "html",
"content": " \n\n\n
\n
\n Ctrl+S saves the current dashboard \n Ctrl+F Opens the dashboard finder \n Ctrl+H Hide/show row controls \n Click and drag graph title to move panel \n Hit Escape to exit graph when in fullscreen or edit mode \n Click the colored icon in the legend to change series color \n Ctrl or Shift + Click legend name to hide other series \n \n
\n
\n",
"style": {},
"title": "Tips & Shortcuts"
}
- ],
- "notice": false
+ ]
},
{
"title": "test",
"height": "250px",
"editable": true,
"collapse": false,
- "collapsable": true,
"panels": [
{
+ "id": 4,
"span": 12,
- "editable": true,
"type": "graph",
"x-axis": true,
"y-axis": true,
@@ -132,27 +124,13 @@
"enable": false
}
}
- ],
- "notice": false
- }
- ],
- "pulldowns": [
- {
- "type": "filtering",
- "collapse": false,
- "notice": false,
- "enable": false
- },
- {
- "type": "annotations",
- "enable": false
+ ]
}
],
"nav": [
{
"type": "timepicker",
"collapse": false,
- "notice": false,
"enable": true,
"status": "Stable",
"time_options": [
@@ -188,5 +166,5 @@
"templating": {
"list": []
},
- "version": 2
-}
\ No newline at end of file
+ "version": 5
+}
diff --git a/src/app/dashboards/empty.json b/src/app/dashboards/empty.json
index fc97a61d125..d1f7ffcd548 100644
--- a/src/app/dashboards/empty.json
+++ b/src/app/dashboards/empty.json
@@ -17,22 +17,10 @@
}
],
"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": [
diff --git a/src/app/dashboards/scripted_async.js b/src/app/dashboards/scripted_async.js
index 84e5d976f6b..31d23f2dde2 100644
--- a/src/app/dashboards/scripted_async.js
+++ b/src/app/dashboards/scripted_async.js
@@ -35,11 +35,9 @@ return function(callback) {
// Set a title
dashboard.title = 'Scripted dash';
- dashboard.services.filter = {
- time: {
- from: "now-" + (ARGS.from || timspan),
- to: "now"
- }
+ dashboard.time = {
+ from: "now-" + (ARGS.from || timspan),
+ to: "now"
};
var rows = 1;
@@ -78,4 +76,4 @@ return function(callback) {
callback(dashboard);
});
-}
\ No newline at end of file
+}
diff --git a/src/app/dashboards/scripted_templated.js b/src/app/dashboards/scripted_templated.js
new file mode 100644
index 00000000000..0ca4ea1fde8
--- /dev/null
+++ b/src/app/dashboards/scripted_templated.js
@@ -0,0 +1,96 @@
+/* global _ */
+
+/*
+ * Complex scripted dashboard
+ * This script generates a dashboard object that Grafana can load. It also takes a number of user
+ * supplied URL parameters (int ARGS variable)
+ *
+ * Return a dashboard object, or a function
+ *
+ * For async scripts, return a function, this function must take a single callback function as argument,
+ * call this callback function with the dashboard object (look at scripted_async.js for an example)
+ */
+
+'use strict';
+
+// accessable variables in this scope
+var window, document, ARGS, $, jQuery, moment, kbn;
+
+// Setup some variables
+var dashboard, timspan;
+
+// All url parameters are available via the ARGS object
+var ARGS;
+
+// Set a default timespan if one isn't specified
+timspan = '1d';
+
+// Intialize a skeleton with nothing but a rows array and service object
+dashboard = {
+ rows : [],
+};
+
+// Set a title
+dashboard.title = 'Scripted dash';
+dashboard.time = {
+ from: "now-" + (ARGS.from || timspan),
+ to: "now"
+};
+dashboard.templating = {
+ enable: true,
+ list: [
+ {
+ name: 'test',
+ query: 'apps.backend.*',
+ refresh: true,
+ options: [],
+ current: null,
+ },
+ {
+ name: 'test2',
+ query: '*',
+ refresh: true,
+ options: [],
+ current: null,
+ }
+ ]
+};
+
+var rows = 1;
+var seriesName = 'argName';
+
+if(!_.isUndefined(ARGS.rows)) {
+ rows = parseInt(ARGS.rows, 10);
+}
+
+if(!_.isUndefined(ARGS.name)) {
+ seriesName = ARGS.name;
+}
+
+for (var i = 0; i < rows; i++) {
+
+ dashboard.rows.push({
+ title: 'Chart',
+ height: '300px',
+ panels: [
+ {
+ title: 'Events',
+ type: 'graph',
+ span: 12,
+ fill: 1,
+ linewidth: 2,
+ targets: [
+ {
+ 'target': "randomWalk('" + seriesName + "')"
+ },
+ {
+ 'target': "randomWalk('[[test2]]')"
+ }
+ ],
+ }
+ ]
+ });
+}
+
+
+return dashboard;
diff --git a/src/app/dashboards/template_vars.json b/src/app/dashboards/template_vars.json
new file mode 100644
index 00000000000..affe7727ce2
--- /dev/null
+++ b/src/app/dashboards/template_vars.json
@@ -0,0 +1,264 @@
+{
+ "id": null,
+ "title": "Templated Graphs Nested",
+ "originalTitle": "Templated Graphs Nested",
+ "tags": [
+ "showcase",
+ "templated"
+ ],
+ "style": "dark",
+ "timezone": "browser",
+ "editable": true,
+ "hideControls": false,
+ "rows": [
+ {
+ "title": "Row1",
+ "height": "350px",
+ "editable": true,
+ "collapse": false,
+ "collapsable": true,
+ "panels": [
+ {
+ "span": 12,
+ "editable": true,
+ "type": "graph",
+ "loadingEditor": false,
+ "datasource": null,
+ "renderer": "flot",
+ "x-axis": true,
+ "y-axis": true,
+ "scale": 1,
+ "y_formats": [
+ "short",
+ "short"
+ ],
+ "grid": {
+ "max": null,
+ "min": 0,
+ "threshold1": null,
+ "threshold2": null,
+ "threshold1Color": "rgba(216, 200, 27, 0.27)",
+ "threshold2Color": "rgba(234, 112, 112, 0.22)",
+ "leftMax": null,
+ "rightMax": null,
+ "leftMin": null,
+ "rightMin": null
+ },
+ "annotate": {
+ "enable": false
+ },
+ "resolution": 100,
+ "lines": true,
+ "fill": 1,
+ "linewidth": 1,
+ "points": false,
+ "pointradius": 5,
+ "bars": false,
+ "stack": true,
+ "legend": {
+ "show": true,
+ "values": false,
+ "min": false,
+ "max": false,
+ "current": false,
+ "total": false,
+ "avg": false
+ },
+ "percentage": false,
+ "zerofill": true,
+ "nullPointMode": "connected",
+ "steppedLine": false,
+ "tooltip": {
+ "value_type": "cumulative",
+ "query_as_alias": true
+ },
+ "targets": [
+ {
+ "target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)",
+ "function": "mean",
+ "column": "value"
+ }
+ ],
+ "aliasColors": {
+ "highres.test": "#1F78C1",
+ "scale(highres.test,3)": "#6ED0E0",
+ "mobile": "#6ED0E0",
+ "tablet": "#EAB839"
+ },
+ "title": "Traffic [[period]]",
+ "id": 1,
+ "seriesOverrides": []
+ }
+ ],
+ "notice": false
+ },
+ {
+ "title": "Row1",
+ "height": "350px",
+ "editable": true,
+ "collapse": false,
+ "collapsable": true,
+ "panels": [
+ {
+ "span": 12,
+ "editable": true,
+ "type": "graph",
+ "loadingEditor": false,
+ "datasource": null,
+ "renderer": "flot",
+ "x-axis": true,
+ "y-axis": true,
+ "scale": 1,
+ "y_formats": [
+ "short",
+ "short"
+ ],
+ "grid": {
+ "max": null,
+ "min": 0,
+ "threshold1": null,
+ "threshold2": null,
+ "threshold1Color": "rgba(216, 200, 27, 0.27)",
+ "threshold2Color": "rgba(234, 112, 112, 0.22)",
+ "leftMax": null,
+ "rightMax": null,
+ "leftMin": null,
+ "rightMin": null
+ },
+ "annotate": {
+ "enable": false
+ },
+ "resolution": 100,
+ "lines": true,
+ "fill": 1,
+ "linewidth": 1,
+ "points": false,
+ "pointradius": 5,
+ "bars": false,
+ "stack": true,
+ "legend": {
+ "show": true,
+ "values": false,
+ "min": false,
+ "max": false,
+ "current": false,
+ "total": false,
+ "avg": false
+ },
+ "percentage": false,
+ "zerofill": true,
+ "nullPointMode": "connected",
+ "steppedLine": false,
+ "tooltip": {
+ "value_type": "cumulative",
+ "query_as_alias": true
+ },
+ "targets": [
+ {
+ "target": "aliasByNode(apps.$app.$server.counters.requests.count, 2)"
+ }
+ ],
+ "aliasColors": {
+ "highres.test": "#1F78C1",
+ "scale(highres.test,3)": "#6ED0E0",
+ "mobile": "#6ED0E0",
+ "tablet": "#EAB839"
+ },
+ "title": "Second pannel",
+ "id": 2,
+ "seriesOverrides": []
+ }
+ ],
+ "notice": 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
+ }
+ ],
+ "time": {
+ "from": "now-15m",
+ "to": "now"
+ },
+ "templating": {
+ "list": [
+ {
+ "type": "query",
+ "name": "app",
+ "query": "apps.*",
+ "includeAll": true,
+ "options": [],
+ "current": {
+ "text": "All",
+ "value": "*"
+ },
+ "datasource": null,
+ "allFormat": "wildcard",
+ "refresh": true
+ },
+ {
+ "type": "query",
+ "name": "server",
+ "query": "apps.$app.*",
+ "includeAll": true,
+ "options": [],
+ "current": {
+ "text": "All",
+ "value": "*"
+ },
+ "datasource": null,
+ "allFormat": "Glob",
+ "refresh": false
+ },
+ {
+ "type": "query",
+ "datasource": null,
+ "refresh_on_load": false,
+ "name": "metric",
+ "options": [],
+ "includeAll": true,
+ "allFormat": "glob",
+ "query": "apps.$app.$server.*",
+ "current": {
+ "text": "counters",
+ "value": "counters"
+ }
+ }
+ ],
+ "enable": true
+ },
+ "annotations": {
+ "enable": false
+ },
+ "refresh": false,
+ "version": 6
+}
diff --git a/src/app/directives/addGraphiteFunc.js b/src/app/directives/addGraphiteFunc.js
index ca5943da508..e66689969ca 100644
--- a/src/app/directives/addGraphiteFunc.js
+++ b/src/app/directives/addGraphiteFunc.js
@@ -38,6 +38,15 @@ function (angular, app, _, $, gfunc) {
items: 10,
updater: function (value) {
var funcDef = gfunc.getFuncDef(value);
+ if (!funcDef) {
+ // try find close match
+ value = value.toLowerCase();
+ funcDef = _.find(allFunctions, function(funcName) {
+ return funcName.toLowerCase().indexOf(value) === 0;
+ });
+
+ if (!funcDef) { return; }
+ }
$scope.$apply(function() {
$scope.addFunction(funcDef);
@@ -97,4 +106,4 @@ function (angular, app, _, $, gfunc) {
};
});
}
-});
\ No newline at end of file
+});
diff --git a/src/app/directives/all.js b/src/app/directives/all.js
index 5236a1a619d..35d718fc942 100644
--- a/src/app/directives/all.js
+++ b/src/app/directives/all.js
@@ -4,6 +4,7 @@ define([
'./grafanaPanel',
'./grafanaSimplePanel',
'./ngBlur',
+ './dashEditLink',
'./ngModelOnBlur',
'./tip',
'./confirmClick',
@@ -14,6 +15,8 @@ define([
'./bodyClass',
'./addGraphiteFunc',
'./graphiteFuncEditor',
+ './templateParamSelector',
+ './graphiteSegment',
'./grafanaVersionCheck',
'./influxdbFuncEditor'
], function () {});
diff --git a/src/app/directives/bodyClass.js b/src/app/directives/bodyClass.js
index 6d3c6d32e15..0b1cac65614 100644
--- a/src/app/directives/bodyClass.js
+++ b/src/app/directives/bodyClass.js
@@ -3,7 +3,7 @@ define([
'app',
'lodash'
],
-function (angular, app, _) {
+function (angular) {
'use strict';
angular
@@ -12,20 +12,14 @@ function (angular, app, _) {
return {
link: function($scope, elem) {
- var lastPulldownVal;
var lastHideControlsVal;
- $scope.$watchCollection('dashboard.pulldowns', function() {
+ $scope.$watch('submenuEnabled', function() {
if (!$scope.dashboard) {
return;
}
- var panel = _.find($scope.dashboard.pulldowns, function(pulldown) { return pulldown.enable; });
- var panelEnabled = panel ? panel.enable : false;
- if (lastPulldownVal !== panelEnabled) {
- elem.toggleClass('submenu-controls-visible', panelEnabled);
- lastPulldownVal = panelEnabled;
- }
+ elem.toggleClass('submenu-controls-visible', $scope.submenuEnabled);
});
$scope.$watch('dashboard.hideControls', function() {
diff --git a/src/app/directives/bootstrap-tagsinput.js b/src/app/directives/bootstrap-tagsinput.js
index 613cc872d78..a8b7eb6a7ad 100644
--- a/src/app/directives/bootstrap-tagsinput.js
+++ b/src/app/directives/bootstrap-tagsinput.js
@@ -102,7 +102,7 @@ function (angular, $) {
var li = '
diff --git a/src/app/panels/timepicker/module.js b/src/app/panels/timepicker/module.js
index 3f29f1652dc..656af0898a0 100644
--- a/src/app/panels/timepicker/module.js
+++ b/src/app/panels/timepicker/module.js
@@ -25,11 +25,11 @@ function (angular, app, _, moment, kbn) {
var module = angular.module('grafana.panels.timepicker', []);
app.useModule(module);
- module.controller('timepicker', function($scope, $modal, $q) {
+ module.controller('timepicker', function($scope, $rootScope, timeSrv) {
+
$scope.panelMeta = {
status : "Stable",
- description : "A panel for controlling the time range filters. If you have time based data, "+
- " or if you're using time stamped indices, you need one of these"
+ description : ""
};
// Set and populate defaults
@@ -39,8 +39,6 @@ function (angular, app, _, moment, kbn) {
refresh_intervals : ['5s','10s','30s','1m','5m','15m','30m','1h','2h','1d'],
};
- var customTimeModal = null;
-
_.defaults($scope.panel,_d);
// ng-pattern regexs
@@ -52,41 +50,36 @@ function (angular, app, _, moment, kbn) {
millisecond: /^[0-9]*$/
};
+ $scope.timeSrv = timeSrv;
+
$scope.$on('refresh', function() {
$scope.init();
});
$scope.init = function() {
- var time = this.filter.timeRange(true);
+ var time = timeSrv.timeRange(true);
if(time) {
- $scope.panel.now = this.filter.timeRange(false).to === "now" ? true : false;
+ $scope.panel.now = timeSrv.timeRange(false).to === "now" ? true : false;
$scope.time = getScopeTimeObj(time.from,time.to);
}
};
$scope.customTime = function() {
- if (!customTimeModal) {
- customTimeModal = $modal({
- template: './app/panels/timepicker/custom.html',
- persist: true,
- show: false,
- scope: $scope,
- keyboard: false
- });
- }
-
// Assume the form is valid since we're setting it to something valid
$scope.input.$setValidity("dummy", true);
$scope.temptime = cloneTime($scope.time);
- $scope.tempnow = $scope.panel.now;
+ $scope.temptime.now = $scope.panel.now;
+
+ $scope.temptime.from.date.setHours(0,0,0,0);
+ $scope.temptime.to.date.setHours(0,0,0,0);
// Date picker needs the date to be at the start of the day
- $scope.temptime.from.date.setHours(1,0,0,0);
- $scope.temptime.to.date.setHours(1,0,0,0);
+ if(new Date().getTimezoneOffset() < 0) {
+ $scope.temptime.from.date = moment($scope.temptime.from.date).add('days',1).toDate();
+ $scope.temptime.to.date = moment($scope.temptime.to.date).add('days',1).toDate();
+ }
- $q.when(customTimeModal).then(function(modalEl) {
- modalEl.modal('show');
- });
+ $scope.emitAppEvent('show-dash-editor', {src: 'app/panels/timepicker/custom.html', scope: $scope });
};
// Constantly validate the input of the fields. This function does not change any date variables
@@ -113,7 +106,7 @@ function (angular, app, _, moment, kbn) {
return false;
}
- return {from:_from,to:_to};
+ return { from: _from, to:_to, now: time.now};
};
$scope.setNow = function() {
@@ -130,12 +123,12 @@ function (angular, app, _, moment, kbn) {
// Create filter object
var _filter = _.clone(time);
- if($scope.tempnow) {
+ if(time.now) {
_filter.to = "now";
}
// Set the filter
- $scope.panel.filter_id = $scope.filter.setTime(_filter);
+ $scope.panel.filter_id = timeSrv.setTime(_filter);
// Update our representation
$scope.time = getScopeTimeObj(time.from,time.to);
@@ -149,7 +142,7 @@ function (angular, app, _, moment, kbn) {
to: "now"
};
- this.filter.setTime(_filter);
+ timeSrv.setTime(_filter);
$scope.time = getScopeTimeObj(kbn.parseDate(_filter.from),new Date());
};
@@ -175,21 +168,21 @@ function (angular, app, _, moment, kbn) {
var model = { from: getTimeObj(from), to: getTimeObj(to), };
if (model.from.date) {
- model.tooltip = moment(model.from.date).format('YYYY-MM-DD HH:mm:ss') + ' to ';
- model.tooltip += moment(model.to.date).format('YYYY-MM-DD HH:mm:ss');
+ model.tooltip = $scope.dashboard.formatDate(model.from.date) + ' to ';
+ model.tooltip += $scope.dashboard.formatDate(model.to.date);
}
else {
model.tooltip = 'Click to set time filter';
}
- if ($scope.filter.time) {
+ if (timeSrv.time) {
if ($scope.panel.now) {
model.rangeString = moment(model.from.date).fromNow() + ' to ' +
moment(model.to.date).fromNow();
}
else {
- model.rangeString = moment(model.from.date).format('MMM D, YYYY hh:mm:ss') + ' to ' +
- moment(model.to.date).format('MMM D, YYYY hh:mm:ss');
+ model.rangeString = $scope.dashboard.formatDate(model.from.date, 'MMM D, YYYY HH:mm:ss') + ' to ' +
+ $scope.dashboard.formatDate(model.to.date, 'MMM D, YYYY HH:mm:ss');
}
}
diff --git a/src/app/partials/annotations_editor.html b/src/app/partials/annotations_editor.html
new file mode 100644
index 00000000000..c72194b6f6a
--- /dev/null
+++ b/src/app/partials/annotations_editor.html
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+ No annotations defined
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ Datasource
+
+
+
+ Icon color
+
+
+
+ Icon size
+
+
+
+ Grid line
+
+
+
+ Line color
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/partials/dashboard.html b/src/app/partials/dashboard.html
index acfd80518e9..f619a74dc85 100644
--- a/src/app/partials/dashboard.html
+++ b/src/app/partials/dashboard.html
@@ -1,127 +1,113 @@
-
+
-
diff --git a/src/app/partials/roweditor.html b/src/app/partials/roweditor.html
index ab66b08356a..032163ea64b 100644
--- a/src/app/partials/roweditor.html
+++ b/src/app/partials/roweditor.html
@@ -1,12 +1,19 @@
-
-
Row settings
+
-
+
+
+
+
+
-
Panels
-
+
Title
Type
- Span ({{dashboard.rowSpan(row)}}/12)
- Delete
- Move
+ Span
+
+
- {{panel.title}}
+ {{panel.title}}
{{panel.type}}
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-