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
\n
\n \n
\n
\n \n
\n
", "style": {}, "title": "Documentation Links" }, { - "error": false, + "id": 3, "span": 6, - "editable": true, "type": "text", "mode": "html", "content": "
\n\n
\n
\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 = '' + '' + (item.text || '') + ''; if (item.submenu && item.submenu.length) { @@ -131,4 +131,4 @@ function (angular, $) { } }; }); -}); \ No newline at end of file +}); diff --git a/src/app/directives/dashEditLink.js b/src/app/directives/dashEditLink.js new file mode 100644 index 00000000000..fbdfc4fa5cc --- /dev/null +++ b/src/app/directives/dashEditLink.js @@ -0,0 +1,84 @@ +define([ + 'angular', + 'jquery' +], +function (angular, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('dashEditorLink', function($timeout) { + return { + restrict: 'A', + link: function(scope, elem, attrs) { + var partial = attrs.dashEditorLink; + + elem.bind('click',function() { + $timeout(function() { + var editorScope = attrs.editorScope === 'isolated' ? null : scope; + scope.emitAppEvent('show-dash-editor', { src: partial, scope: editorScope }); + }); + }); + } + }; + }); + + angular + .module('grafana.directives') + .directive('dashEditorView', function($compile) { + return { + restrict: 'A', + link: function(scope, elem) { + var editorScope; + var lastEditor; + + function hideScrollbars(value) { + if (value) { + document.documentElement.style.overflow = 'hidden'; // firefox, chrome + document.body.scroll = "no"; // ie only + } else { + document.documentElement.style.overflow = 'auto'; + document.body.scroll = "yes"; + } + } + + function hideEditorPane() { + hideScrollbars(false); + if (editorScope) { editorScope.dismiss(); } + } + + scope.onAppEvent("dashboard-loaded", hideEditorPane); + scope.onAppEvent('hide-dash-editor', hideEditorPane); + + scope.onAppEvent('show-dash-editor', function(evt, payload) { + hideEditorPane(); + + if (lastEditor === payload.src) { return; } + + scope.exitFullscreen(); + + lastEditor = payload.src; + editorScope = payload.scope ? payload.scope.$new() : scope.$new(); + + editorScope.dismiss = function() { + editorScope.$destroy(); + elem.empty(); + lastEditor = null; + editorScope = null; + hideScrollbars(false); + }; + + // hide page scrollbars while edit pane is visible + hideScrollbars(true); + + var src = "'" + payload.src + "'"; + var view = $('
'); + elem.append(view); + $compile(elem.contents())(editorScope); + }); + + } + }; + }); + +}); diff --git a/src/app/directives/dashUpload.js b/src/app/directives/dashUpload.js index ad76cb5e780..1d7c4ec405e 100644 --- a/src/app/directives/dashUpload.js +++ b/src/app/directives/dashUpload.js @@ -15,8 +15,9 @@ function (angular) { var readerOnload = function() { return function(e) { var dashboard = JSON.parse(e.target.result); - scope.emitAppEvent('setup-dashboard', dashboard); - scope.$apply(); + scope.$apply(function() { + scope.emitAppEvent('setup-dashboard', dashboard); + }); }; }; for (var i = 0, f; f = files[i]; i++) { diff --git a/src/app/directives/grafanaGraph.js b/src/app/directives/grafanaGraph.js index a950032a7a9..15f1556aa62 100755 --- a/src/app/directives/grafanaGraph.js +++ b/src/app/directives/grafanaGraph.js @@ -10,12 +10,12 @@ function (angular, $, kbn, moment, _) { var module = angular.module('grafana.directives'); - module.directive('grafanaGraph', function($rootScope) { + module.directive('grafanaGraph', function($rootScope, timeSrv) { return { restrict: 'A', template: '
', link: function(scope, elem) { - var data, plot, annotations; + var data, annotations; var hiddenData = {}; var dashboard = scope.dashboard; var legendSideLastValue = null; @@ -46,11 +46,6 @@ function (angular, $, kbn, moment, _) { render_panel(); }); - // Re-render if the window is resized - angular.element(window).bind('resize', function() { - render_panel(); - }); - function setElementHeight() { try { var height = scope.height || scope.panel.height || scope.row.height; @@ -87,6 +82,10 @@ function (angular, $, kbn, moment, _) { render_panel_as_graphite_png(data); return true; } + + if (elem.width() === 0) { + return; + } } // Function for rendering panel @@ -118,7 +117,7 @@ function (angular, $, kbn, moment, _) { lines: { show: panel.lines, zero: false, - fill: panel.fill === 0 ? 0.001 : panel.fill/10, + fill: translateFillOption(panel.fill), lineWidth: panel.linewidth, steps: panel.steppedLine }, @@ -154,11 +153,12 @@ function (angular, $, kbn, moment, _) { }; for (var i = 0; i < data.length; i++) { - var _d = data[i].getFlotPairs(panel.nullPointMode, panel.y_formats); - data[i].data = _d; + var series = data[i]; + series.applySeriesOverrides(panel.seriesOverrides); + series.data = series.getFlotPairs(panel.nullPointMode, panel.y_formats); } - if (panel.bars && data.length && data[0].info.timeStep) { + if (data.length && data[0].info.timeStep) { options.series.bars.barWidth = data[0].info.timeStep / 1.5; } @@ -167,19 +167,29 @@ function (angular, $, kbn, moment, _) { addAnnotations(options); configureAxisOptions(data, options); - // if legend is to the right delay plot draw a few milliseconds - // so the legend width calculation can be done - if (shouldDelayDraw(panel)) { - legendSideLastValue = panel.legend.rightSide; - setTimeout(function() { - plot = $.plot(elem, data, options); - addAxisLabels(); - }, 50); - } - else { - plot = $.plot(elem, data, options); + var sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); + + function callPlot() { + try { + $.plot(elem, sortedSeries, options); + } catch (e) { + console.log('flotcharts error', e); + } + addAxisLabels(); } + + if (shouldDelayDraw(panel)) { + setTimeout(callPlot, 50); + legendSideLastValue = panel.legend.rightSide; + } + else { + callPlot(); + } + } + + function translateFillOption(fill) { + return fill === 0 ? 0.001 : fill/10; } function shouldDelayDraw(panel) { @@ -353,11 +363,8 @@ function (angular, $, kbn, moment, _) { value = item.datapoint[1]; } - value = kbn.getFormatFunction(format, 2)(value); - - timestamp = dashboard.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'); + value = kbn.getFormatFunction(format, 2)(value, item.series.yaxis); + timestamp = dashboard.formatDate(item.datapoint[0]); $tooltip.html(group + value + " @ " + timestamp).place_tt(pos.pageX, pos.pageY); } else { @@ -417,7 +424,7 @@ function (angular, $, kbn, moment, _) { elem.bind("plotselected", function (event, ranges) { scope.$apply(function() { - scope.filter.setTime({ + timeSrv.setTime({ from : moment.utc(ranges.xaxis.from).toDate(), to : moment.utc(ranges.xaxis.to).toDate(), }); diff --git a/src/app/directives/grafanaPanel.js b/src/app/directives/grafanaPanel.js index 0229277b038..5f56fd67b35 100644 --- a/src/app/directives/grafanaPanel.js +++ b/src/app/directives/grafanaPanel.js @@ -18,8 +18,8 @@ function (angular, $) { '
' + '
' + '' + - '' + + 'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' + + '' + '' + '' + '' + @@ -40,7 +40,7 @@ function (angular, $) { 'onStop:\'panelMoveStop\''+ '}" ng-model="panel" ' + '>' + - '{{panel.title || "No title"}}' + + '{{panel.title | interpolateTemplateVars}}' + '' + ''+ diff --git a/src/app/directives/grafanaVersionCheck.js b/src/app/directives/grafanaVersionCheck.js index 185a5f6668b..487265e26af 100644 --- a/src/app/directives/grafanaVersionCheck.js +++ b/src/app/directives/grafanaVersionCheck.js @@ -30,4 +30,4 @@ function (angular) { } }; }); -}); \ No newline at end of file +}); diff --git a/src/app/directives/graphiteFuncEditor.js b/src/app/directives/graphiteFuncEditor.js index 8aa0551b18a..dff8003f54f 100644 --- a/src/app/directives/graphiteFuncEditor.js +++ b/src/app/directives/graphiteFuncEditor.js @@ -8,7 +8,7 @@ function (angular, _, $) { angular .module('grafana.directives') - .directive('graphiteFuncEditor', function($compile) { + .directive('graphiteFuncEditor', function($compile, templateSrv) { var funcSpanTemplate = '{{func.def.name}}('; var paramTemplate = ', ').appendTo(elem); } - var $paramLink = $('' + func.params[index] + ''); + var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); + var $paramLink = $('' + paramValue + ''); var $input = $(paramTemplate); paramCountAtLink++; @@ -239,4 +239,4 @@ function (angular, _, $) { }); -}); \ No newline at end of file +}); diff --git a/src/app/directives/graphiteSegment.js b/src/app/directives/graphiteSegment.js new file mode 100644 index 00000000000..f0116bc8847 --- /dev/null +++ b/src/app/directives/graphiteSegment.js @@ -0,0 +1,134 @@ +define([ + 'angular', + 'app', + 'lodash', + 'jquery', +], +function (angular, app, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('graphiteSegment', function($compile, $sce) { + var inputTemplate = ''; + + var buttonTemplate = ''; + + return { + link: function($scope, elem) { + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + var segment = $scope.segment; + var options = null; + var cancelBlur = null; + + $input.appendTo(elem); + $button.appendTo(elem); + + $scope.updateVariableValue = function(value) { + if (value === '' || segment.value === value) { + return; + } + + $scope.$apply(function() { + var selected = _.findWhere($scope.altSegments, { value: value }); + if (selected) { + segment.value = selected.value; + segment.html = selected.html; + segment.expandable = selected.expandable; + } + else { + segment.value = value; + segment.html = $sce.trustAsHtml(value); + segment.expandable = true; + } + $scope.segmentValueChanged(segment, $scope.$index); + }); + }; + + $scope.switchToLink = function(now) { + if (now === true || cancelBlur) { + clearTimeout(cancelBlur); + cancelBlur = null; + $input.hide(); + $button.show(); + $scope.updateVariableValue($input.val()); + } + else { + // need to have long delay because the blur + // happens long before the click event on the typeahead options + cancelBlur = setTimeout($scope.switchToLink, 350); + } + }; + + $scope.source = function(query, callback) { + if (options) { return options; } + + $scope.$apply(function() { + $scope.getAltSegments($scope.$index).then(function() { + options = _.map($scope.altSegments, function(alt) { return alt.value; }); + + // add custom values + if (segment.value !== 'select metric' && _.indexOf(options, segment.value) === -1) { + options.unshift(segment.value); + } + + callback(options); + }); + }); + }; + + $scope.updater = function(value) { + if (value === segment.value) { + clearTimeout(cancelBlur); + $input.focus(); + return value; + } + + $input.val(value); + $scope.switchToLink(true); + + return value; + }; + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ source: $scope.source, minLength: 0, items: 10000, updater: $scope.updater }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + this.query = this.$element.val() || ''; + var items = this.source(this.query, $.proxy(this.process, this)); + return items ? this.process(items) : items; + }; + + $button.keydown(function(evt) { + // trigger typeahead on down arrow or enter key + if (evt.keyCode === 40 || evt.keyCode === 13) { + $button.click(); + } + }); + + $button.click(function() { + options = null; + $input.css('width', ($button.width() + 16) + 'px'); + + $button.hide(); + $input.show(); + $input.focus(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + }); + + $input.blur($scope.switchToLink); + + $compile(elem.contents())($scope); + } + }; + }); +}); diff --git a/src/app/directives/ngModelOnBlur.js b/src/app/directives/ngModelOnBlur.js index 2a107e71808..0e9d94a282e 100644 --- a/src/app/directives/ngModelOnBlur.js +++ b/src/app/directives/ngModelOnBlur.js @@ -7,13 +7,14 @@ function (angular) { .directive('ngModelOnblur', function() { return { restrict: 'A', + priority: 1, require: 'ngModel', link: function(scope, elm, attr, ngModelCtrl) { if (attr.type === 'radio' || attr.type === 'checkbox') { return; } - elm.unbind('input').unbind('keydown').unbind('change'); + elm.off('input keydown change'); elm.bind('blur', function() { scope.$apply(function() { ngModelCtrl.$setViewValue(elm.val()); @@ -22,4 +23,4 @@ function (angular) { } }; }); -}); \ No newline at end of file +}); diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js new file mode 100644 index 00000000000..194dfbdcb40 --- /dev/null +++ b/src/app/directives/templateParamSelector.js @@ -0,0 +1,82 @@ +define([ + 'angular', + 'app', + 'lodash', + 'jquery', +], +function (angular, app, _, $) { + 'use strict'; + + angular + .module('grafana.directives') + .directive('templateParamSelector', function($compile) { + var inputTemplate = ''; + + var buttonTemplate = '{{variable.current.text}}'; + + return { + link: function($scope, elem) { + var $input = $(inputTemplate); + var $button = $(buttonTemplate); + var variable = $scope.variable; + + $input.appendTo(elem); + $button.appendTo(elem); + + function updateVariableValue(value) { + $scope.$apply(function() { + var selected = _.findWhere(variable.options, { text: value }); + if (!selected) { + selected = { text: value, value: value }; + } + $scope.setVariableValue($scope.variable, selected); + }); + } + + $input.attr('data-provide', 'typeahead'); + $input.typeahead({ + minLength: 0, + items: 1000, + updater: function(value) { + $input.val(value); + $input.trigger('blur'); + return value; + } + }); + + var typeahead = $input.data('typeahead'); + typeahead.lookup = function () { + var options = _.map(variable.options, function(option) { return option.text; }); + this.query = this.$element.val() || ''; + return this.process(options); + }; + + $button.click(function() { + $input.css('width', ($button.width() + 16) + 'px'); + + $button.hide(); + $input.show(); + $input.focus(); + + var typeahead = $input.data('typeahead'); + if (typeahead) { + $input.val(''); + typeahead.lookup(); + } + + }); + + $input.blur(function() { + if ($input.val() !== '') { updateVariableValue($input.val()); } + $input.hide(); + $button.show(); + $button.focus(); + }); + + $compile(elem.contents())($scope); + } + }; + }); +}); diff --git a/src/app/directives/tip.js b/src/app/directives/tip.js index 975a4c02228..974ed98a637 100644 --- a/src/app/directives/tip.js +++ b/src/app/directives/tip.js @@ -11,10 +11,10 @@ function (angular, kbn) { return { restrict: 'E', link: function(scope, elem, attrs) { - var _t = ''; elem.replaceWith($compile(angular.element(_t))(scope)); } }; }); -}); \ No newline at end of file +}); diff --git a/src/app/filters/all.js b/src/app/filters/all.js index 926051d4a0d..eb9a736d0ce 100755 --- a/src/app/filters/all.js +++ b/src/app/filters/all.js @@ -9,18 +9,6 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen }; }); - /* - Filter an array of objects by elasticsearch version requirements - */ - module.filter('esVersion', function(esVersion) { - return function(items, require) { - var ret = _.filter(items,function(qt) { - return esVersion.is(qt[require]) ? true : false; - }); - return ret; - }; - }); - module.filter('slice', function() { return function(arr, start, end) { if(!_.isUndefined(arr)) { @@ -67,51 +55,10 @@ define(['angular', 'jquery', 'lodash', 'moment'], function (angular, $, _, momen }; }); - module.filter('urlLink', function() { - var //URLs starting with http://, https://, or ftp:// - r1 = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim, - //URLs starting with "www." (without // before it, or it'd re-link the ones done above). - r2 = /(^|[^\/])(www\.[\S]+(\b|$))/gim, - //Change email addresses to mailto:: links. - r3 = /(\w+@[a-zA-Z_]+?\.[a-zA-Z]{2,6})/gim; - - var urlLink = function(text) { - var t1,t2,t3; - if(!_.isString(text)) { - return text; - } else { - _.each(text.match(r1), function() { - t1 = text.replace(r1, "$1"); - }); - text = t1 || text; - _.each(text.match(r2), function() { - t2 = text.replace(r2, "$1$2"); - }); - text = t2 || text; - _.each(text.match(r3), function() { - t3 = text.replace(r3, "$1"); - }); - text = t3 || text; - return text; - } - }; + module.filter('interpolateTemplateVars', function(templateSrv) { return function(text) { - return _.isArray(text) - ? _.map(text, urlLink) - : urlLink(text); + return templateSrv.replaceWithText(text); }; }); - module.filter('gistid', function() { - var gist_pattern = /(\d{5,})|([a-z0-9]{10,})|(gist.github.com(\/*.*)\/[a-z0-9]{5,}\/*$)/; - return function(input) { - if(!(_.isUndefined(input))) { - var output = input.match(gist_pattern); - if(!_.isNull(output) && !_.isUndefined(output)) { - return output[0].replace(/.*\//, ''); - } - } - }; - }); - -}); \ No newline at end of file +}); diff --git a/src/app/panels/annotations/editor.html b/src/app/panels/annotations/editor.html deleted file mode 100644 index e1184193009..00000000000 --- a/src/app/panels/annotations/editor.html +++ /dev/null @@ -1,67 +0,0 @@ -
- - - -
diff --git a/src/app/panels/annotations/module.html b/src/app/panels/annotations/module.html deleted file mode 100644 index 0441ed1f883..00000000000 --- a/src/app/panels/annotations/module.html +++ /dev/null @@ -1,12 +0,0 @@ -
- - - - - -
\ No newline at end of file diff --git a/src/app/panels/annotations/module.js b/src/app/panels/annotations/module.js deleted file mode 100644 index fe55e27303c..00000000000 --- a/src/app/panels/annotations/module.js +++ /dev/null @@ -1,39 +0,0 @@ -/* - - ## annotations - -*/ -define([ - 'angular', - 'app', - 'lodash', - './editor' -], -function (angular, app, _) { - 'use strict'; - - var module = angular.module('grafana.panels.annotations', []); - app.useModule(module); - - module.controller('AnnotationsCtrl', function($scope, datasourceSrv, $rootScope) { - - $scope.panelMeta = { - status : "Stable", - description : "Annotations" - }; - - // Set and populate defaults - var _d = { - annotations: [] - }; - - _.defaults($scope.panel, _d); - - $scope.hide = function (annotation) { - annotation.enable = !annotation.enable; - $rootScope.$broadcast('refresh'); - }; - - }); - -}); diff --git a/src/app/panels/filtering/module.html b/src/app/panels/filtering/module.html deleted file mode 100755 index 96b5751ce7d..00000000000 --- a/src/app/panels/filtering/module.html +++ /dev/null @@ -1,50 +0,0 @@ -
- -
- -
-
- - -
- -
- -
- -
-
    -
  • - name:
    - -
  • -
  • - filter.query:
    - -
  • -
  • - - -
  • -
-
- - -
-
-
- -
-
diff --git a/src/app/panels/filtering/module.js b/src/app/panels/filtering/module.js deleted file mode 100644 index a8f64cad6ed..00000000000 --- a/src/app/panels/filtering/module.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - - ## filtering - -*/ -define([ - 'angular', - 'app', - 'lodash' -], -function (angular, app, _) { - 'use strict'; - - var module = angular.module('grafana.panels.filtering', []); - app.useModule(module); - - module.controller('filtering', function($scope, datasourceSrv, $rootScope, $timeout, $q) { - - $scope.panelMeta = { - status : "Stable", - description : "graphite target filters" - }; - - // Set and populate defaults - var _d = { - }; - _.defaults($scope.panel,_d); - - $scope.init = function() { - // empty. Don't know if I need the function then. - }; - - $scope.remove = function(templateParameter) { - $scope.filter.removeTemplateParameter(templateParameter); - }; - - $scope.filterOptionSelected = function(templateParameter, option, recursive) { - templateParameter.current = option; - - $scope.filter.updateTemplateData(); - - return $scope.applyFilterToOtherFilters(templateParameter) - .then(function() { - // only refresh in the outermost call - if (!recursive) { - $scope.dashboard.emit_refresh(); - } - }); - }; - - $scope.applyFilterToOtherFilters = function(updatedTemplatedParam) { - var promises = _.map($scope.filter.templateParameters, function(templateParam) { - if (templateParam === updatedTemplatedParam) { - return; - } - if (templateParam.query.indexOf('[[' + updatedTemplatedParam.name + ']]') !== -1) { - return $scope.applyFilter(templateParam); - } - }); - - return $q.all(promises); - }; - - $scope.applyFilter = function(templateParam) { - return datasourceSrv.default.metricFindQuery($scope.filter, templateParam.query) - .then(function (results) { - templateParam.editing = undefined; - templateParam.options = _.map(results, function(node) { - return { text: node.text, value: node.text }; - }); - - if (templateParam.includeAll) { - var allExpr = '{'; - _.each(templateParam.options, function(option) { - allExpr += option.text + ','; - }); - allExpr = allExpr.substring(0, allExpr.length - 1) + '}'; - templateParam.options.unshift({text: 'All', value: allExpr}); - } - - // if parameter has current value - // if it exists in options array keep value - if (templateParam.current) { - var currentExists = _.findWhere(templateParam.options, { value: templateParam.current.value }); - if (currentExists) { - return $scope.filterOptionSelected(templateParam, templateParam.current, true); - } - } - - return $scope.filterOptionSelected(templateParam, templateParam.options[0], true); - }); - }; - - $scope.add = function() { - $scope.filter.addTemplateParameter({ - type : 'filter', - name : 'filter name', - editing : true, - query : 'metric.path.query.*', - }); - }; - - }); -}); diff --git a/src/app/panels/graph/legend.html b/src/app/panels/graph/legend.html index 5524a8047fe..0e5edc459ce 100755 --- a/src/app/panels/graph/legend.html +++ b/src/app/panels/graph/legend.html @@ -34,12 +34,12 @@
- -
diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 647e52c257e..aa6667ee154 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -1,16 +1,3 @@ -/** @scratch /panels/5 - * include::panels/histogram.asciidoc[] - */ - -/** @scratch /panels/histogram/0 - * == Histogram - * Status: *Stable* - * - * The histogram panel allow for the display of time charts. It includes several modes and tranformations - * to display event counts, mean, min, max and total of numeric fields, and derivatives of counter - * fields. - * - */ define([ 'angular', 'app', @@ -18,7 +5,8 @@ define([ 'lodash', 'kbn', 'moment', - './timeSeries', + 'components/timeSeries', + './seriesOverridesCtrl', 'services/panelSrv', 'services/annotationsSrv', 'services/datasourceSrv', @@ -29,14 +17,13 @@ define([ 'jquery.flot.stack', 'jquery.flot.stackpercent' ], -function (angular, app, $, _, kbn, moment, timeSeries) { - +function (angular, app, $, _, kbn, moment, TimeSeries) { 'use strict'; - var module = angular.module('grafana.panels.graph', []); + var module = angular.module('grafana.panels.graph'); app.useModule(module); - module.controller('GraphCtrl', function($scope, $rootScope, $timeout, panelSrv, annotationsSrv) { + module.controller('GraphCtrl', function($scope, $rootScope, panelSrv, annotationsSrv, timeSrv) { $scope.panelMeta = { modals : [], @@ -179,7 +166,8 @@ function (angular, app, $, _, kbn, moment, timeSeries) { targets: [{}], aliasColors: {}, - aliasYAxis: {}, + + seriesOverrides: [], }; _.defaults($scope.panel,_d); @@ -191,16 +179,10 @@ function (angular, app, $, _, kbn, moment, timeSeries) { $scope.hiddenSeries = {}; $scope.updateTimeRange = function () { - $scope.range = $scope.filter.timeRange(); - $scope.rangeUnparsed = $scope.filter.timeRange(false); + $scope.range = timeSrv.timeRange(); + $scope.rangeUnparsed = timeSrv.timeRange(false); $scope.resolution = Math.ceil($(window).width() * ($scope.panel.span / 12)); - $scope.interval = '10m'; - - if ($scope.range) { - $scope.interval = kbn.secondsToHms( - kbn.calculate_interval($scope.range.from, $scope.range.to, $scope.resolution, 0) / 1000 - ); - } + $scope.interval = kbn.calculateInterval($scope.range, $scope.resolution, $scope.panel.interval); }; $scope.get_data = function() { @@ -215,13 +197,13 @@ function (angular, app, $, _, kbn, moment, timeSeries) { cacheTimeout: $scope.panel.cacheTimeout }; - $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.filter, $scope.rangeUnparsed, $scope.dashboard); + $scope.annotationsPromise = annotationsSrv.getAnnotations($scope.rangeUnparsed, $scope.dashboard); - return $scope.datasource.query($scope.filter, metricsQuery) + return $scope.datasource.query(metricsQuery) .then($scope.dataHandler) .then(null, function(err) { $scope.panelMeta.loading = false; - $scope.panel.error = err.message || "Timeseries data request error"; + $scope.panelMeta.error = err.message || "Timeseries data request error"; $scope.inspector.error = err; $scope.render([]); }); @@ -258,18 +240,15 @@ function (angular, app, $, _, kbn, moment, timeSeries) { var datapoints = seriesData.datapoints; var alias = seriesData.target; var color = $scope.panel.aliasColors[alias] || $rootScope.colors[index]; - var yaxis = $scope.panel.aliasYAxis[alias] || 1; var seriesInfo = { alias: alias, color: color, - enable: true, - yaxis: yaxis }; $scope.legend.push(seriesInfo); - var series = new timeSeries.ZeroFilled({ + var series = new TimeSeries({ datapoints: datapoints, info: seriesInfo, }); @@ -347,8 +326,12 @@ function (angular, app, $, _, kbn, moment, timeSeries) { }; $scope.toggleYAxis = function(info) { - info.yaxis = info.yaxis === 2 ? 1 : 2; - $scope.panel.aliasYAxis[info.alias] = info.yaxis; + var override = _.findWhere($scope.panel.seriesOverrides, { alias: info.alias }); + if (!override) { + override = { alias: info.alias }; + $scope.panel.seriesOverrides.push(override); + } + override.yaxis = info.yaxis === 2 ? 1 : 2; $scope.render(); }; @@ -357,6 +340,23 @@ function (angular, app, $, _, kbn, moment, timeSeries) { $scope.render(); }; + $scope.addSeriesOverride = function() { + $scope.panel.seriesOverrides.push({}); + }; + + $scope.removeSeriesOverride = function(override) { + $scope.panel.seriesOverrides = _.without($scope.panel.seriesOverrides, override); + $scope.render(); + }; + + $scope.toggleEditorHelp = function(index) { + if ($scope.editorHelpIndex === index) { + $scope.editorHelpIndex = null; + return; + } + $scope.editorHelpIndex = index; + }; + panelSrv.init($scope); }); diff --git a/src/app/panels/graph/seriesOverridesCtrl.js b/src/app/panels/graph/seriesOverridesCtrl.js new file mode 100644 index 00000000000..4f54c6d0e6f --- /dev/null +++ b/src/app/panels/graph/seriesOverridesCtrl.js @@ -0,0 +1,80 @@ +define([ + 'angular', + 'app', + 'lodash', +], function(angular, app, _) { + 'use strict'; + + var module = angular.module('grafana.panels.graph', []); + app.useModule(module); + + module.controller('SeriesOverridesCtrl', function($scope) { + $scope.overrideMenu = []; + $scope.currentOverrides = []; + $scope.override = $scope.override || {}; + + $scope.addOverrideOption = function(name, propertyName, values) { + var option = {}; + option.text = name; + option.propertyName = propertyName; + option.index = $scope.overrideMenu.length; + option.values = values; + + option.submenu = _.map(values, function(value, index) { + return { + text: String(value), + click: 'setOverride(' + option.index + ',' + index + ')' + }; + }); + + $scope.overrideMenu.push(option); + }; + + $scope.setOverride = function(optionIndex, valueIndex) { + var option = $scope.overrideMenu[optionIndex]; + var value = option.values[valueIndex]; + $scope.override[option.propertyName] = value; + $scope.updateCurrentOverrides(); + $scope.render(); + }; + + $scope.removeOverride = function(option) { + delete $scope.override[option.propertyName]; + $scope.updateCurrentOverrides(); + $scope.render(); + }; + + $scope.getSeriesNames = function() { + return _.map($scope.legend, function(info) { + return info.alias; + }); + }; + + $scope.updateCurrentOverrides = function() { + $scope.currentOverrides = []; + _.each($scope.overrideMenu, function(option) { + var value = $scope.override[option.propertyName]; + if (_.isUndefined(value)) { return; } + $scope.currentOverrides.push({ + name: option.text, + propertyName: option.propertyName, + value: String(value) + }); + }); + }; + + $scope.addOverrideOption('Bars', 'bars', [true, false]); + $scope.addOverrideOption('Lines', 'lines', [true, false]); + $scope.addOverrideOption('Line fill', 'fill', [0,1,2,3,4,5,6,7,8,9,10]); + $scope.addOverrideOption('Line width', 'linewidth', [0,1,2,3,4,5,6,7,8,9,10]); + $scope.addOverrideOption('Staircase line', 'steppedLine', [true, false]); + $scope.addOverrideOption('Points', 'points', [true, false]); + $scope.addOverrideOption('Points Radius', 'pointradius', [1,2,3,4,5]); + $scope.addOverrideOption('Stack', 'stack', [true, false]); + $scope.addOverrideOption('Y-axis', 'yaxis', [1, 2]); + $scope.addOverrideOption('Z-index', 'zindex', [-1,-2,-3,0,1,2,3]); + $scope.updateCurrentOverrides(); + + }); + +}); diff --git a/src/app/panels/graph/styleEditor.html b/src/app/panels/graph/styleEditor.html index ec18121cba7..cd83f23f197 100644 --- a/src/app/panels/graph/styleEditor.html +++ b/src/app/panels/graph/styleEditor.html @@ -1,5 +1,3 @@ - -
Chart Options
@@ -29,7 +27,7 @@
- +
@@ -64,3 +62,44 @@
+ +
+
+
Series specific overrides Regex match example: /server[0-3]/i
+
+
+
+
    +
  • + +
  • + +
  • + alias or regex +
  • +
  • + +
  • +
  • + + {{option.name}}: {{option.value}} +
  • + +
+
+
+
+
+ + +
+
diff --git a/src/app/panels/text/editor.html b/src/app/panels/text/editor.html index 6af4dc069c2..b3b8afbbec0 100644 --- a/src/app/panels/text/editor.html +++ b/src/app/panels/text/editor.html @@ -9,10 +9,9 @@
- - \ No newline at end of file + diff --git a/src/app/panels/text/module.html b/src/app/panels/text/module.html index 626692ad2f9..a184b8283d8 100644 --- a/src/app/panels/text/module.html +++ b/src/app/panels/text/module.html @@ -1,4 +1,4 @@
-

+

diff --git a/src/app/panels/text/module.js b/src/app/panels/text/module.js index c745cf4f30a..e652b40a56b 100644 --- a/src/app/panels/text/module.js +++ b/src/app/panels/text/module.js @@ -3,7 +3,6 @@ define([ 'app', 'lodash', 'require', - 'services/filterSrv' ], function (angular, app, _, require) { 'use strict'; @@ -13,7 +12,7 @@ function (angular, app, _, require) { var converter; - module.controller('text', function($scope, filterSrv, $sce, panelSrv) { + module.controller('text', function($scope, templateSrv, $sce, panelSrv) { $scope.panelMeta = { description : "A static text panel that can use plain text, markdown, or (sanitized) HTML" @@ -76,7 +75,7 @@ function (angular, app, _, require) { $scope.updateContent = function(html) { try { - $scope.content = $sce.trustAsHtml(filterSrv.applyTemplateToTarget(html)); + $scope.content = $sce.trustAsHtml(templateSrv.replace(html)); } catch(e) { console.log('Text panel error: ', e); $scope.content = $sce.trustAsHtml(html); diff --git a/src/app/panels/timepicker/custom.html b/src/app/panels/timepicker/custom.html index 0784cf987e0..5497d0b6549 100644 --- a/src/app/panels/timepicker/custom.html +++ b/src/app/panels/timepicker/custom.html @@ -1,78 +1,84 @@ - - - + diff --git a/src/app/panels/timepicker/module.html b/src/app/panels/timepicker/module.html index 6a498da2959..8357f66a7a3 100644 --- a/src/app/panels/timepicker/module.html +++ b/src/app/panels/timepicker/module.html @@ -16,8 +16,7 @@ @@ -45,7 +44,7 @@
  • - +
  • 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 @@ +
    + +
    +
    + + Annotations +
    + +
    +
    +
    +
    + +
    + +
    +
    +
    +
    + No annotations defined +
    + + + + + + + + +
    +   + {{annotation.name}} + + + + Edit + + + + + +
    +
    +
    + +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    + +
    +
    + + +
    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 @@ -
    +
    -
    -
    +
    + +
      +
    • + +
    • - cacheTimeout Graphite parameter to overwride memcache default timeout (unit is seconds) + cacheTimeout
    • + class="input-mini grafana-target-segment-input" + ng-model="panel.cacheTimeout" + bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'" + data-placement="right" + spellcheck='false' + placeholder="60">
    -
    +
    +
    +
    + +
    +
    + +
    +
    Shorter legend names
    +
      +
    • alias() function to specify a custom series name
    • +
    • aliasByNode(2) to alias by a specific part of your metric path
    • +
    • aliasByNode(2, -1) you can add multiple segment paths, and use negative index
    • +
    • groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by
    • +
    +
    + +
    +
    Series as parameter
    +
      +
    • Some graphite functions allow you to have many series arguments
    • +
    • Use #[A-Z] to use a graphite query as parameter to a function
    • +
    • + Examples: +
        +
      • asPercent(#A, #B)
      • +
      • prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query
      • +
      • prod.srv-01.counters.count - sumSeries(#A) : sum count and series A
      • +
      • divideSeries(#A, #B)
      • +
      +
    • +
    • If a query is added only to be used as a parameter, hide it from the graph with the eye icon
    • +
    +
    + +
    +
    Stacking
    +
      +
    • You find the stacking option under Display Styles tab
    • +
    • When stacking is enabled make sure null point mode is set to 'null as zero'
    • +
    +
    + +
    +
    Templating
    +
      +
    • You can use a template variable in place of metric names
    • +
    • You can use a template variable in place of function parameters
    • +
    • You enable the templating feature in Dashboard settings / Feature toggles
    • +
    +
    + +
    diff --git a/src/app/partials/import.html b/src/app/partials/import.html index eeb481e9bdf..f81468465d4 100644 --- a/src/app/partials/import.html +++ b/src/app/partials/import.html @@ -1,4 +1,4 @@ -
    +
    Import dashboards from graphite web
    @@ -12,7 +12,7 @@
    - +
    diff --git a/src/app/partials/influxdb/annotation_editor.html b/src/app/partials/influxdb/annotation_editor.html index 9bc2bdbca21..fe867d68f36 100644 --- a/src/app/partials/influxdb/annotation_editor.html +++ b/src/app/partials/influxdb/annotation_editor.html @@ -1,8 +1,8 @@
    -
    InfluxDB Query Example: select text from events where [[timeFilter]]
    +
    InfluxDB Query Example: select text from events where $timeFilter
    - +
    diff --git a/src/app/partials/influxdb/editor.html b/src/app/partials/influxdb/editor.html index 8bc3baf8ff7..4fd13645d3d 100644 --- a/src/app/partials/influxdb/editor.html +++ b/src/app/partials/influxdb/editor.html @@ -1,5 +1,4 @@ - -
    +
    - + -
      +
      • - +
      • @@ -45,7 +42,7 @@
        • - -
        • - as -
        • - -
        • - -
        +
        +
    + +
    + +
      +
    • + +
    • +
    • + alias +
    • -
    • -
    • - - - -
    • - -
    • - -
    • - -
    • - as -
    • - -
    • -
    • +
    • + group by time +
    • +
    • + +
    • +
    + + +
    +
    -
    -
    - - alias patterns: - -
      -
    • $s = series name
    • -
    • $g = group by
    • -
    • $[0-9] part of series name for series names seperated by dots.
    • -
        -
    -
    +
    +
    +
    +
      +
    • + +
    • +
    • + group by time +
    • +
    • + +
    • +
    • + +
    • +
    +
    +
    + + +
    +
    + +
    +
    + +
    +
    Alias patterns
    +
      +
    • $s = series name
    • +
    • $g = group by
    • +
    • $[0-9] part of series name for series names seperated by dots.
    • +
    +
    + +
    +
    Stacking and fill
    +
      +
    • When stacking is enabled it important that points align
    • +
    • If there are missing points for one series it can cause gaps or missing bars
    • +
    • You must use fill(0), and select a group by time low limit
    • +
    • Use the group by time option below your queries and specify for example >10s if your metrics are written every 10 seconds
    • +
    • This will insert zeros for series that are missing measurements and will make stacking work properly
    • +
    +
    + +
    +
    Group by time
    +
      +
    • Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana
    • +
    • Leave the group by time field empty for each query and it will be calculated based on time range and pixel width of the graph
    • +
    • If you use fill(0) or fill(null) set a low limit for the auto group by time interval
    • +
    • The low limit can only be set in the group by time option below your queries
    • +
    • You set a low limit by adding a greater sign before the interval
    • +
    • Example: >60s if you write metrics to InfluxDB every 60 seconds
    • +
    +
    + + +
    +
    + + diff --git a/src/app/partials/inspector.html b/src/app/partials/inspector.html index 7f2ec526f70..79ca806d9f0 100644 --- a/src/app/partials/inspector.html +++ b/src/app/partials/inspector.html @@ -1,69 +1,80 @@ + diff --git a/src/app/partials/opentsdb/editor.html b/src/app/partials/opentsdb/editor.html index bce27f230f2..b74b1d7a269 100644 --- a/src/app/partials/opentsdb/editor.html +++ b/src/app/partials/opentsdb/editor.html @@ -12,7 +12,7 @@ - + -