diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c6d39929f8..1909b01c7bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,16 +1,67 @@ # 1.8.0 (unreleased) -**New features and improvements** +**Fixes** +- [Issue #802](https://github.com/grafana/grafana/issues/802). Annotations: Fix when using InfluxDB datasource +- [Issue #795](https://github.com/grafana/grafana/issues/795). Chrome: Fix for display issue in chrome beta & chrome canary when entering edit mode +# 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 #277](https://github.com/grafana/grafana/issues/277). Dashboard: Fix for timepicker date & tooltip when UTC timezone selected. Closes #277 +- [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; @@ -22,7 +73,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) @@ -194,7 +245,7 @@ Read this for more info: - More graphite function definitions - Make "ms" axis format include hour, day, weeks, month and year ([Issue #149](https://github.com/grafana/grafana/issues/149)) - Microsecond axis format ([Issue #146](https://github.com/grafana/grafana/issues/146)) -- Specify template paramaters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123)) +- Specify template parameters in URL ([Issue #123](https://github.com/grafana/grafana/issues/123)) ### Fixes - Basic Auth fix ([Issue #152](https://github.com/grafana/grafana/issues/152)) 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 eecf826776e..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" diff --git a/src/app/components/kbn.js b/src/app/components/kbn.js index 46a1b8dfa79..cefcb42d289 100644 --- a/src/app/components/kbn.js +++ b/src/app/components/kbn.js @@ -8,25 +8,6 @@ function($, _, moment) { 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 @@ -131,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])) { @@ -652,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 b43237224b4..6afba222b4b 100644 --- a/src/app/components/settings.js +++ b/src/app/components/settings.js @@ -14,6 +14,7 @@ function (_, crypto) { */ var defaults = { datasources : {}, + window_title_prefix : 'Grafana - ', panels : ['graph', 'text'], plugins : {}, default_route : '/dashboard/file/default.json', diff --git a/src/app/components/timeSeries.js b/src/app/components/timeSeries.js index 85c27f1da88..4c58c211cc3 100644 --- a/src/app/components/timeSeries.js +++ b/src/app/components/timeSeries.js @@ -15,8 +15,7 @@ function (_, kbn) { if (!aliasOrRegex) { return false; } if (aliasOrRegex[0] === '/') { - var match = aliasOrRegex.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); - var regex = new RegExp(match[1], match[2]); + var regex = kbn.stringToJsRegex(aliasOrRegex); return seriesAlias.match(regex) != null; } 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 7132ca8a571..424f0e225e8 100644 --- a/src/app/controllers/dashboardCtrl.js +++ b/src/app/controllers/dashboardCtrl.js @@ -11,60 +11,62 @@ function (angular, $, config, _) { var module = angular.module('grafana.controllers'); module.controller('DashboardCtrl', function( - $scope, $rootScope, dashboardKeybindings, - filterSrv, dashboardSrv, dashboardViewStateSrv, - panelMoveSrv, timer, $timeout) { + $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(function() { - $scope.$broadcast('render'); - }); + $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; @@ -91,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(".","/"); @@ -99,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 a58a2647945..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,15 +92,14 @@ function (angular, _, moment, config, store) { }); }; - $scope.deleteDashboard = function(id, $event) { - $event.stopPropagation(); - + $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() { @@ -108,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(), }); }; @@ -135,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 935ee1aeef0..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 de0ec44045a..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,6 +57,10 @@ function (angular, _, config, $) { } }; + $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); }; @@ -65,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; }); }; @@ -83,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; @@ -94,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) { @@ -126,8 +140,7 @@ function (angular, _, config, $) { }); }; - $scope.toggleImport = function ($event) { - $event.stopPropagation(); + $scope.toggleImport = function () { $scope.showImport = !$scope.showImport; }; @@ -139,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..70b907e13da 100644 --- a/src/app/dashboards/default.json +++ b/src/app/dashboards/default.json @@ -8,64 +8,56 @@ { "title": "New row", "height": "150px", - "editable": true, "collapse": false, - "collapsable": true, + "editable": true, "panels": [ { - "error": false, + "id": 1, "span": 12, "editable": true, "type": "text", "mode": "html", - "content": "
\n \n
", + "content": "
\n \n
", "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 6898b838845..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); 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 222bde78b26..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; @@ -82,6 +82,10 @@ function (angular, $, kbn, moment, _) { render_panel_as_graphite_png(data); return true; } + + if (elem.width() === 0) { + return; + } } // Function for rendering panel @@ -165,18 +169,22 @@ function (angular, $, kbn, moment, _) { var sortedSeries = _.sortBy(data, function(series) { return series.zindex; }); - // if legend is to the right delay plot draw a few milliseconds - // so the legend width calculation can be done + 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; - setTimeout(function() { - plot = $.plot(elem, sortedSeries, options); - addAxisLabels(); - }, 50); } else { - plot = $.plot(elem, sortedSeries, options); - addAxisLabels(); + callPlot(); } } @@ -355,7 +363,7 @@ function (angular, $, kbn, moment, _) { value = item.datapoint[1]; } - value = kbn.getFormatFunction(format, 2)(value); + 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); @@ -416,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/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/module.html b/src/app/panels/graph/module.html index 0d418e5e8cd..b4302bbd0f2 100644 --- a/src/app/panels/graph/module.html +++ b/src/app/panels/graph/module.html @@ -21,14 +21,23 @@
-
-
-
-
-
+
+
+
+ + Graph +
-
-
-
-
+
+
+
+
+
+ +
+
+
+
+
+
diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 6897cc090b0..aa6667ee154 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -23,7 +23,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries) { 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,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() { @@ -203,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([]); }); @@ -355,6 +349,14 @@ function (angular, app, $, _, kbn, moment, TimeSeries) { $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/styleEditor.html b/src/app/panels/graph/styleEditor.html index de7318d4500..cd83f23f197 100644 --- a/src/app/panels/graph/styleEditor.html +++ b/src/app/panels/graph/styleEditor.html @@ -27,7 +27,7 @@
- +
@@ -67,34 +67,30 @@
Series specific overrides Regex match example: /server[0-3]/i
-
-
+
- -
    +
    • -
    -
    • alias or regex
    • + ng-model="override.alias" + bs-typeahead="getSeriesNames" + ng-blur="render()" + data-min-length=0 data-items=100 + class="input-medium grafana-target-segment-input" >
    • {{option.name}}: {{option.value}}
    • @@ -103,7 +99,6 @@
-
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 f00ad5f7a87..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,33 +50,25 @@ 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); @@ -89,9 +79,7 @@ function (angular, app, _, moment, kbn) { $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 @@ -118,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() { @@ -135,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); @@ -154,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()); }; @@ -187,7 +175,7 @@ function (angular, app, _, moment, kbn) { 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(); 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 @@ -
    +
    - +
    +
    +
    +
      +
    • + +
    • +
    • + 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 @@ - + -
      +
    -
    -
    -
    {{tab.title}}
    -
    -
    \ No newline at end of file diff --git a/src/app/partials/paneleditor.html b/src/app/partials/paneleditor.html index 62692c5f88e..ebc8a40ac63 100644 --- a/src/app/partials/paneleditor.html +++ b/src/app/partials/paneleditor.html @@ -1,23 +1,30 @@ - \ No newline at end of file +
    diff --git a/src/app/partials/playlist.html b/src/app/partials/playlist.html index 576938d8543..3b3ba7600c4 100644 --- a/src/app/partials/playlist.html +++ b/src/app/partials/playlist.html @@ -1,55 +1,61 @@
    - - -
    \ No newline at end of file +
    +
    + + Start dashboard playlist +
    +
    + +
    + +
    +
    +
    + + + + + + + + + + + + + + +
    DashboardIncludeRemove as favorite
    + {{dashboard.title}} + + + + +
    + No dashboards marked as favorites +
    +
    +
    +
    + + dashboards available in the playlist are only the once marked as favorites (stored in local browser storage). + to mark a dashboard as favorite, use save icon in the menu and in the dropdown select mark as favorite +

    +
    +
    +
    +
    + + +
    +
    +
    +
    + + + diff --git a/src/app/partials/roweditor.html b/src/app/partials/roweditor.html index ab66b08356a..032163ea64b 100644 --- a/src/app/partials/roweditor.html +++ b/src/app/partials/roweditor.html @@ -1,12 +1,19 @@ -
    - + +
    +
    +
    +
    Local File Load dashboard JSON layout from file
    +
    +
    +
    +
    +
    +
    + + diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html new file mode 100644 index 00000000000..42884a84058 --- /dev/null +++ b/src/app/partials/submenu.html @@ -0,0 +1,49 @@ + diff --git a/src/app/partials/templating_editor.html b/src/app/partials/templating_editor.html new file mode 100644 index 00000000000..90d1fb50d26 --- /dev/null +++ b/src/app/partials/templating_editor.html @@ -0,0 +1,170 @@ +
    +
    + + Templating +
    + +
    +
    +
    +
    + +
    + +
    + +
    + +
    +
    +
    + No template variables defined +
    + + + + + + + + + +
    + + ${{variable.name}} + + + {{variable.query}} + + + + Edit + + + + + +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    + + +
    +
    +
    + +
    +
    +
    + + +
    +
    +
    + +
    +
    +
    + + + +
    +
    + +
    +
    + + + +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    +
    +
    + +
      +
    • + {{option.text}} +
    • +
    +
    +
    +
    +
    + +
    + +
    + + +
    + + + + + + + + + + + diff --git a/src/app/routes/dashboard-from-db.js b/src/app/routes/dashboard-from-db.js index d2c274fc57a..a5a37ece12d 100644 --- a/src/app/routes/dashboard-from-db.js +++ b/src/app/routes/dashboard-from-db.js @@ -34,9 +34,9 @@ function (angular) { .then(function(dashboard) { $scope.emitAppEvent('setup-dashboard', dashboard); }).then(null, function(error) { + $scope.emitAppEvent('setup-dashboard', { title: 'Grafana'}); alertSrv.set('Error', error, 'error'); }); - }); }); diff --git a/src/app/services/all.js b/src/app/services/all.js index 62deab62e5e..81fe4603277 100644 --- a/src/app/services/all.js +++ b/src/app/services/all.js @@ -1,7 +1,9 @@ define([ './alertSrv', './datasourceSrv', - './filterSrv', + './timeSrv', + './templateSrv', + './templateValuesSrv', './panelSrv', './timer', './panelMove', diff --git a/src/app/services/annotationsSrv.js b/src/app/services/annotationsSrv.js index a219caa5d66..c1733189462 100644 --- a/src/app/services/annotationsSrv.js +++ b/src/app/services/annotationsSrv.js @@ -9,7 +9,6 @@ define([ module.service('annotationsSrv', function(datasourceSrv, $q, alertSrv, $rootScope) { var promiseCached; - var annotationPanel; var list = []; var timezone; @@ -22,9 +21,8 @@ define([ list = []; }; - this.getAnnotations = function(filterSrv, rangeUnparsed, dashboard) { - annotationPanel = _.findWhere(dashboard.pulldowns, { type: 'annotations' }); - if (!annotationPanel.enable) { + this.getAnnotations = function(rangeUnparsed, dashboard) { + if (!dashboard.annotations.enable) { return $q.when(null); } @@ -33,12 +31,12 @@ define([ } timezone = dashboard.timezone; - var annotations = _.where(annotationPanel.annotations, { enable: true }); + var annotations = _.where(dashboard.annotations.list, { enable: true }); var promises = _.map(annotations, function(annotation) { var datasource = datasourceSrv.get(annotation.datasource); - return datasource.annotationQuery(annotation, filterSrv, rangeUnparsed) + return datasource.annotationQuery(annotation, rangeUnparsed) .then(this.receiveAnnotationResults) .then(null, errorHandler); }, this); diff --git a/src/app/services/dashboard/dashboardKeyBindings.js b/src/app/services/dashboard/dashboardKeyBindings.js index da285e48809..ba8dcaf0f23 100644 --- a/src/app/services/dashboard/dashboardKeyBindings.js +++ b/src/app/services/dashboard/dashboardKeyBindings.js @@ -18,11 +18,11 @@ function(angular, $) { keyboardManager.unbind('ctrl+s'); keyboardManager.unbind('ctrl+r'); keyboardManager.unbind('ctrl+z'); + keyboardManager.unbind('esc'); }); - keyboardManager.unbind('esc'); - keyboardManager.bind('ctrl+f', function(evt) { - scope.emitAppEvent('open-search', evt); + keyboardManager.bind('ctrl+f', function() { + scope.emitAppEvent('show-dash-editor', { src: 'app/partials/search.html' }); }, { inputDisabled: true }); keyboardManager.bind('ctrl+h', function() { @@ -53,6 +53,8 @@ function(angular, $) { modalData.$scope.dismiss(); } + scope.emitAppEvent('hide-dash-editor'); + scope.exitFullscreen(); }, { inputDisabled: true }); }; diff --git a/src/app/services/dashboard/dashboardSrv.js b/src/app/services/dashboard/dashboardSrv.js index 6a363837bc3..c6b386917ba 100644 --- a/src/app/services/dashboard/dashboardSrv.js +++ b/src/app/services/dashboard/dashboardSrv.js @@ -11,7 +11,7 @@ function (angular, $, kbn, _, moment) { var module = angular.module('grafana.services'); - module.factory('dashboardSrv', function(timer, $rootScope, $timeout) { + module.factory('dashboardSrv', function($rootScope) { function DashboardModel (data) { @@ -25,12 +25,13 @@ function (angular, $, kbn, _, moment) { this.tags = data.tags || []; this.style = data.style || "dark"; this.timezone = data.timezone || 'browser'; - this.editable = data.editble || true; + this.editable = data.editable || true; + this.hideControls = data.hideControls || false; this.rows = data.rows || []; - this.pulldowns = data.pulldowns || []; this.nav = data.nav || []; this.time = data.time || { from: 'now-6h', to: 'now' }; - this.templating = data.templating || { list: [] }; + this.templating = data.templating || { list: [], enable: false }; + this.annotations = data.annotations || { list: [], enable: false}; this.refresh = data.refresh; this.version = data.version || 0; @@ -38,14 +39,6 @@ function (angular, $, kbn, _, moment) { this.nav.push({ type: 'timepicker' }); } - if (!_.findWhere(this.pulldowns, {type: 'filtering'})) { - this.pulldowns.push({ type: 'filtering', enable: false }); - } - - if (!_.findWhere(this.pulldowns, {type: 'annotations'})) { - this.pulldowns.push({ type: 'annotations', enable: false }); - } - this.updateSchema(data); } @@ -122,34 +115,13 @@ function (angular, $, kbn, _, moment) { $rootScope.$broadcast('refresh'); }; - p.start_scheduled_refresh = function (after_ms) { - this.cancel_scheduled_refresh(); - this.refresh_timer = timer.register($timeout(function () { - this.start_scheduled_refresh(after_ms); - this.emit_refresh(); - }.bind(this), after_ms)); - }; - - p.cancel_scheduled_refresh = function () { - timer.cancel(this.refresh_timer); - }; - - p.set_interval = function (interval) { - this.refresh = interval; - if (interval) { - var _i = kbn.interval_to_ms(interval); - this.start_scheduled_refresh(_i); - } else { - this.cancel_scheduled_refresh(); - } - }; - p.updateSchema = function(old) { + var i, j, k; var oldVersion = this.version; var panelUpgrades = []; - this.version = 4; + this.version = 6; - if (oldVersion === 4) { + if (oldVersion === 6) { return; } @@ -159,7 +131,7 @@ function (angular, $, kbn, _, moment) { if (old.services) { if (old.services.filter) { this.time = old.services.filter.time; - this.templating.list = old.services.filter.list; + this.templating.list = old.services.filter.list || []; } delete this.services; } @@ -224,14 +196,38 @@ function (angular, $, kbn, _, moment) { }); } + if (oldVersion < 6) { + // move pulldowns to new schema + var filtering = _.findWhere(old.pulldowns, { type: 'filtering' }); + var annotations = _.findWhere(old.pulldowns, { type: 'annotations' }); + if (filtering) { + this.templating.enable = filtering.enable; + } + if (annotations) { + this.annotations = { + list: annotations.annotations, + enable: annotations.enable + }; + } + + // update template variables + for (i = 0 ; i < this.templating.list.length; i++) { + var variable = this.templating.list[i]; + if (variable.datasource === void 0) { variable.datasource = null; } + if (variable.type === 'filter') { variable.type = 'query'; } + if (variable.type === void 0) { variable.type = 'query'; } + if (variable.allFormat === void 0) { variable.allFormat = 'glob'; } + } + } + if (panelUpgrades.length === 0) { return; } - for (var i = 0; i < this.rows.length; i++) { + for (i = 0; i < this.rows.length; i++) { var row = this.rows[i]; - for (var j = 0; j < row.panels.length; j++) { - for (var k = 0; k < panelUpgrades.length; k++) { + for (j = 0; j < row.panels.length; j++) { + for (k = 0; k < panelUpgrades.length; k++) { panelUpgrades[k](row.panels[j]); } } diff --git a/src/app/services/dashboard/dashboardViewStateSrv.js b/src/app/services/dashboard/dashboardViewStateSrv.js index b39e159ae4c..2f85da5d27a 100644 --- a/src/app/services/dashboard/dashboardViewStateSrv.js +++ b/src/app/services/dashboard/dashboardViewStateSrv.js @@ -14,15 +14,16 @@ function (angular, _, $) { // like fullscreen panel & edit function DashboardViewState($scope) { var self = this; + self.state = {}; + self.panelScopes = []; + self.$scope = $scope; $scope.exitFullscreen = function() { - self.update({ fullscreen: false }); + if (self.state.fullscreen) { + self.update({ fullscreen: false }); + } }; - $scope.onAppEvent('dashboard-saved', function() { - self.update({ fullscreen: false }); - }); - $scope.onAppEvent('$routeUpdate', function() { var urlState = self.getQueryStringState(); if (self.needsSync(urlState)) { @@ -30,42 +31,48 @@ function (angular, _, $) { } }); - this.panelScopes = []; - this.$scope = $scope; - this.update(this.getQueryStringState(), true); } DashboardViewState.prototype.needsSync = function(urlState) { - if (urlState.fullscreen !== this.fullscreen) { return true; } - if (urlState.edit !== this.edit) { return true; } - if (urlState.panelId !== this.panelId) { return true; } - return false; + return _.isEqual(this.state, urlState) === false; }; DashboardViewState.prototype.getQueryStringState = function() { var queryParams = $location.search(); - return { + var urlState = { panelId: parseInt(queryParams.panelId) || null, fullscreen: queryParams.fullscreen ? true : false, - edit: queryParams.edit ? true : false + edit: queryParams.edit ? true : false, }; + + _.each(queryParams, function(value, key) { + if (key.indexOf('var-') !== 0) { return; } + urlState[key] = value; + }); + + return urlState; + }; + + DashboardViewState.prototype.serializeToUrl = function() { + var urlState = _.clone(this.state); + urlState.fullscreen = this.state.fullscreen ? true : null, + urlState.edit = this.state.edit ? true : null; + + return urlState; }; DashboardViewState.prototype.update = function(state, skipUrlSync) { - _.extend(this, state); + _.extend(this.state, state); + this.fullscreen = this.state.fullscreen; - if (!this.fullscreen) { - this.panelId = null; - this.edit = false; + if (!this.state.fullscreen) { + this.state.panelId = null; + this.state.edit = false; } if (!skipUrlSync) { - $location.search({ - fullscreen: this.fullscreen ? true : null, - panelId: this.panelId, - edit: this.edit ? true : null - }); + $location.search(this.serializeToUrl()); } this.syncState(); @@ -78,7 +85,7 @@ function (angular, _, $) { if (this.fullscreenPanel) { this.leaveFullscreen(false); } - var panelScope = this.getPanelScope(this.panelId); + var panelScope = this.getPanelScope(this.state.panelId); this.enterFullscreen(panelScope); return; } @@ -120,8 +127,8 @@ function (angular, _, $) { var fullscreenHeight = Math.floor(docHeight * 0.7); this.oldTimeRange = panelScope.range; - panelScope.height = this.edit ? editHeight : fullscreenHeight; - panelScope.editMode = this.edit; + panelScope.height = this.state.edit ? editHeight : fullscreenHeight; + panelScope.editMode = this.state.edit; this.fullscreenPanel = panelScope; $(window).scrollTop(0); @@ -137,7 +144,7 @@ function (angular, _, $) { var self = this; self.panelScopes.push(panelScope); - if (self.panelId === panelScope.panel.id) { + if (self.state.panelId === panelScope.panel.id) { self.enterFullscreen(panelScope); } diff --git a/src/app/services/datasourceSrv.js b/src/app/services/datasourceSrv.js index 96461a81a2c..dfe701780be 100644 --- a/src/app/services/datasourceSrv.js +++ b/src/app/services/datasourceSrv.js @@ -13,7 +13,7 @@ function (angular, _, config) { var module = angular.module('grafana.services'); - module.service('datasourceSrv', function($q, filterSrv, $http, $injector) { + module.service('datasourceSrv', function($q, $http, $injector) { var datasources = {}; var metricSources = []; var annotationSources = []; @@ -21,10 +21,12 @@ function (angular, _, config) { this.init = function() { _.each(config.datasources, function(value, key) { - datasources[key] = this.datasourceFactory(value); + var ds = this.datasourceFactory(value); if (value.default) { - this.default = datasources[key]; + this.default = ds; + ds.default = true; } + datasources[key] = ds; }, this); if (!this.default) { @@ -38,6 +40,7 @@ function (angular, _, config) { metricSources.push({ name: value.name, value: value.default ? null : key, + default: value.default, }); } if (value.supportAnnotations) { @@ -78,7 +81,7 @@ function (angular, _, config) { if (!name) { return this.default; } if (datasources[name]) { return datasources[name]; } - throw "Unable to find datasource: " + name; + return this.default; }; this.getAnnotationSources = function() { diff --git a/src/app/services/elasticsearch/es-datasource.js b/src/app/services/elasticsearch/es-datasource.js index 8b499c75af1..80e3c5d0100 100644 --- a/src/app/services/elasticsearch/es-datasource.js +++ b/src/app/services/elasticsearch/es-datasource.js @@ -1,17 +1,16 @@ define([ 'angular', 'lodash', - 'jquery', 'config', 'kbn', 'moment' ], -function (angular, _, $, config, kbn, moment) { +function (angular, _, config, kbn, moment) { 'use strict'; var module = angular.module('grafana.services'); - module.factory('ElasticDatasource', function($q, $http) { + module.factory('ElasticDatasource', function($q, $http, templateSrv) { function ElasticDatasource(datasource) { this.type = 'elastic'; @@ -60,7 +59,7 @@ function (angular, _, $, config, kbn, moment) { }); }; - ElasticDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) { + ElasticDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { var range = {}; var timeField = annotation.timeField || '@timestamp'; var queryString = annotation.query || '*'; @@ -73,10 +72,14 @@ function (angular, _, $, config, kbn, moment) { to: rangeUnparsed.to, }; - var queryInterpolated = filterSrv.applyTemplateToTarget(queryString); + var queryInterpolated = templateSrv.replace(queryString); var filter = { "bool": { "must": [{ "range": range }] } }; var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } }; - var data = { "query" : { "filtered": { "query" : query, "filter": filter } }, "size": 100 }; + var data = { + "fields": [timeField, "_source"], + "query" : { "filtered": { "query" : query, "filter": filter } }, + "size": 100 + }; return this._request('POST', '/_search', annotation.index, data).then(function(results) { var list = []; @@ -84,9 +87,16 @@ function (angular, _, $, config, kbn, moment) { for (var i = 0; i < hits.length; i++) { var source = hits[i]._source; + var fields = hits[i].fields; + var time = source[timeField]; + + if (_.isString(fields[timeField]) || _.isNumber(fields[timeField])) { + time = fields[timeField]; + } + var event = { annotation: annotation, - time: moment.utc(source[timeField]).valueOf(), + time: moment.utc(time).valueOf(), title: source[titleField], }; @@ -108,25 +118,29 @@ function (angular, _, $, config, kbn, moment) { }); }; + ElasticDatasource.prototype._getDashboardWithSlug = function(id) { + return this._get('/dashboard/' + kbn.slugifyForUrl(id)) + .then(function(result) { + return angular.fromJson(result._source.dashboard); + }, function() { + throw "Dashboard not found"; + }); + }; + ElasticDatasource.prototype.getDashboard = function(id, isTemp) { var url = '/dashboard/' + id; + if (isTemp) { url = '/temp/' + id; } - if (isTemp) { - url = '/temp/' + id; - } - + var self = this; return this._get(url) .then(function(result) { - if (result._source && result._source.dashboard) { - return angular.fromJson(result._source.dashboard); - } else { - return false; - } + return angular.fromJson(result._source.dashboard); }, function(data) { if(data.status === 0) { throw "Could not contact Elasticsearch. Please ensure that Elasticsearch is reachable from your browser."; } else { - throw "Could not find dashboard " + id; + // backward compatible fallback + return self._getDashboardWithSlug(id); } }); }; @@ -148,15 +162,29 @@ function (angular, _, $, config, kbn, moment) { return this._saveTempDashboard(data); } else { - return this._request('PUT', '/dashboard/' + encodeURIComponent(title), this.index, data) - .then(function() { - return { title: title, url: '/dashboard/db/' + title }; - }, function(err) { - throw 'Failed to save to elasticsearch ' + err.data; + + var id = encodeURIComponent(kbn.slugifyForUrl(title)); + var self = this; + + return this._request('PUT', '/dashboard/' + id, this.index, data) + .then(function(results) { + self._removeUnslugifiedDashboard(results, title); + return { title: title, url: '/dashboard/db/' + id }; + }, function() { + throw 'Failed to save to elasticsearch'; }); } }; + ElasticDatasource.prototype._removeUnslugifiedDashboard = function(saveResult, title) { + if (saveResult.statusText !== 'Created') { return; } + + var self = this; + this._get('/dashboard/' + title).then(function() { + self.deleteDashboard(title); + }); + }; + ElasticDatasource.prototype._saveTempDashboard = function(data) { return this._request('POST', '/temp/?ttl=' + this.saveTempTTL, this.index, data) .then(function(result) { @@ -181,7 +209,21 @@ function (angular, _, $, config, kbn, moment) { }; ElasticDatasource.prototype.searchDashboards = function(queryString) { - queryString = queryString.toLowerCase().replace(' and ', ' AND '); + var endsInOpen = function(string, opener, closer) { + var character; + var count = 0; + for (var i=0; i 0; + }; var tagsOnly = queryString.indexOf('tags!:') === 0; if (tagsOnly) { @@ -193,7 +235,21 @@ function (angular, _, $, config, kbn, moment) { queryString = 'title:'; } - if (queryString[queryString.length - 1] !== '*') { + // make this a partial search if we're not in some reserved portion of the language, comments on conditionals, in order: + // 1. ends in reserved character, boosting, boolean operator ( -foo) + // 2. typing a reserved word like AND, OR, NOT + // 3. open parens (groupiing) + // 4. open " (term phrase) + // 5. open [ (range) + // 6. open { (range) + // see http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax + if (!queryString.match(/(\*|\]|}|~|\)|"|^\d+|\s[\-+]\w+)$/) && + !queryString.match(/[A-Z]$/) && + !endsInOpen(queryString, '(', ')') && + !endsInOpen(queryString, '"', '"') && + !endsInOpen(queryString, '[', ']') && !endsInOpen(queryString, '[', '}') && + !endsInOpen(queryString, '{', ']') && !endsInOpen(queryString, '{', '}') + ){ queryString += '*'; } } @@ -216,7 +272,7 @@ function (angular, _, $, config, kbn, moment) { for (var i = 0; i < results.hits.hits.length; i++) { hits.dashboards.push({ id: results.hits.hits[i]._id, - title: results.hits.hits[i]._id, + title: results.hits.hits[i]._source.title, tags: results.hits.hits[i]._source.tags }); } diff --git a/src/app/services/filterSrv.js b/src/app/services/filterSrv.js deleted file mode 100644 index 6b7787c7940..00000000000 --- a/src/app/services/filterSrv.js +++ /dev/null @@ -1,98 +0,0 @@ -define([ - 'angular', - 'lodash', - 'config', - 'kbn' -], function (angular, _, config, kbn) { - 'use strict'; - - var module = angular.module('grafana.services'); - - module.factory('filterSrv', function($rootScope, $timeout, $routeParams) { - var result = { - - updateTemplateData: function(initial) { - var _templateData = {}; - _.each(this.templateParameters, function(templateParameter) { - if (initial) { - var urlValue = $routeParams[ templateParameter.name ]; - if (urlValue) { - templateParameter.current = { text: urlValue, value: urlValue }; - } - } - if (!templateParameter.current || !templateParameter.current.value) { - return; - } - _templateData[templateParameter.name] = templateParameter.current.value; - }); - this._templateData = _templateData; - }, - - addTemplateParameter: function(templateParameter) { - this.templateParameters.push(templateParameter); - this.updateTemplateData(); - }, - - applyTemplateToTarget: function(target) { - if (!target || target.indexOf('[[') === -1) { - return target; - } - - return _.template(target, this._templateData, this.templateSettings); - }, - - setTime: function(time) { - _.extend(this.time, time); - - // disable refresh if we have an absolute time - if (time.to !== 'now') { - this.old_refresh = this.dashboard.refresh; - this.dashboard.set_interval(false); - } - else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) { - this.dashboard.set_interval(this.old_refresh); - this.old_refresh = null; - } - - $timeout(this.dashboard.emit_refresh, 0); - }, - - timeRange: function(parse) { - var _t = this.time; - if(_.isUndefined(_t) || _.isUndefined(_t.from)) { - return false; - } - if(parse === false) { - return { - from: _t.from, - to: _t.to - }; - } else { - var _from = _t.from; - var _to = _t.to || new Date(); - - return { - from : kbn.parseDate(_from), - to : kbn.parseDate(_to) - }; - } - }, - - removeTemplateParameter: function(templateParameter) { - this.templateParameters = _.without(this.templateParameters, templateParameter); - this.dashboard.templating.list = this.templateParameters; - }, - - init: function(dashboard) { - this.dashboard = dashboard; - this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g }; - this.time = dashboard.time; - this.templateParameters = dashboard.templating.list; - this.updateTemplateData(true); - } - }; - - return result; - }); - -}); diff --git a/src/app/services/graphite/gfunc.js b/src/app/services/graphite/gfunc.js index 9339999f475..533bb897283 100644 --- a/src/app/services/graphite/gfunc.js +++ b/src/app/services/graphite/gfunc.js @@ -24,6 +24,14 @@ function (_) { index[funcDef.shortName || funcDef.name] = funcDef; } + var optionalSeriesRefArgs = [ + { name: 'other', type: 'value_or_series', optional: true }, + { name: 'other', type: 'value_or_series', optional: true }, + { name: 'other', type: 'value_or_series', optional: true }, + { name: 'other', type: 'value_or_series', optional: true }, + { name: 'other', type: 'value_or_series', optional: true } + ]; + addFuncDef({ name: 'scaleToSeconds', category: categories.Transform, @@ -58,20 +66,40 @@ function (_) { }); addFuncDef({ - name: 'sumSeries', - shortName: 'sum', - category: categories.Combine, + name: 'diffSeries', + params: optionalSeriesRefArgs, + defaultParams: ['#A'], + category: categories.Calculate, }); addFuncDef({ - name: 'diffSeries', + name: 'divideSeries', + params: optionalSeriesRefArgs, + defaultParams: ['#A'], + category: categories.Calculate, + }); + + addFuncDef({ + name: 'asPercent', + params: optionalSeriesRefArgs, + defaultParams: ['#A'], + category: categories.Calculate, + }); + + addFuncDef({ + name: 'sumSeries', + shortName: 'sum', category: categories.Combine, + params: optionalSeriesRefArgs, + defaultParams: [''], }); addFuncDef({ name: 'averageSeries', shortName: 'avg', category: categories.Combine, + params: optionalSeriesRefArgs, + defaultParams: [''], }); addFuncDef({ @@ -280,8 +308,8 @@ function (_) { addFuncDef({ name: 'nonNegativeDerivative', category: categories.Transform, - params: [{ name: "max value or 0", type: "int", }], - defaultParams: [0] + params: [{ name: "max value or 0", type: "int", optional: true }], + defaultParams: [''] }); addFuncDef({ @@ -482,23 +510,35 @@ function (_) { categories[catName] = _.sortBy(funcList, 'name'); }); - function FuncInstance(funcDef) { + function FuncInstance(funcDef, options) { this.def = funcDef; - this.params = funcDef.defaultParams.slice(0); + this.params = []; + + if (options && options.withDefaultParams) { + this.params = funcDef.defaultParams.slice(0); + } + this.updateText(); } FuncInstance.prototype.render = function(metricExp) { var str = this.def.name + '('; - var parameters = _.map(this.params, function(value) { - return _.isString(value) ? "'" + value + "'" : value; - }); + var parameters = _.map(this.params, function(value, index) { - if (metricExp !== undefined) { + var paramType = this.def.params[index].type; + if (paramType === 'int' || paramType === 'value_or_series') { + return value; + } + + return "'" + value + "'"; + + }, this); + + if (metricExp) { parameters.unshift(metricExp); } - return str + parameters.join(',') + ')'; + return str + parameters.join(', ') + ')'; }; FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) { @@ -522,9 +562,6 @@ function (_) { if (strValue === '' && this.def.params[index].optional) { this.params.splice(index, 1); } - else if (this.def.params[index].type === 'int') { - this.params[index] = parseFloat(strValue, 10); - } else { this.params[index] = strValue; } @@ -539,27 +576,20 @@ function (_) { } var text = this.def.name + '('; - _.each(this.def.params, function(param, index) { - if (param.optional && this.params[index] === undefined) { - return; - } - - text += this.params[index] + ', '; - }, this); - text = text.substring(0, text.length - 2); + text += this.params.join(', '); text += ')'; this.text = text; }; return { - createFuncInstance: function(funcDef) { + createFuncInstance: function(funcDef, options) { if (_.isString(funcDef)) { if (!index[funcDef]) { throw { message: 'Method not found ' + name }; } funcDef = index[funcDef]; } - return new FuncInstance(funcDef); + return new FuncInstance(funcDef, options); }, getFuncDef: function(name) { diff --git a/src/app/services/graphite/graphiteDatasource.js b/src/app/services/graphite/graphiteDatasource.js index 761b29b3a9f..20ef5cbfa3a 100644 --- a/src/app/services/graphite/graphiteDatasource.js +++ b/src/app/services/graphite/graphiteDatasource.js @@ -11,7 +11,7 @@ function (angular, _, $, config, kbn, moment) { var module = angular.module('grafana.services'); - module.factory('GraphiteDatasource', function($q, $http) { + module.factory('GraphiteDatasource', function($q, $http, templateSrv) { function GraphiteDatasource(datasource) { this.type = 'graphite'; @@ -26,7 +26,7 @@ function (angular, _, $, config, kbn, moment) { this.cacheTimeout = datasource.cacheTimeout; } - GraphiteDatasource.prototype.query = function(filterSrv, options) { + GraphiteDatasource.prototype.query = function(options) { try { var graphOptions = { from: this.translateTime(options.range.from, 'round-down'), @@ -37,7 +37,7 @@ function (angular, _, $, config, kbn, moment) { maxDataPoints: options.maxDataPoints, }; - var params = this.buildGraphiteParams(filterSrv, graphOptions); + var params = this.buildGraphiteParams(graphOptions); if (options.format === 'png') { return $q.when(this.url + '/render' + '?' + params.join('&')); @@ -60,10 +60,10 @@ function (angular, _, $, config, kbn, moment) { } }; - GraphiteDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) { + GraphiteDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { // Graphite metric as annotation if (annotation.target) { - var target = filterSrv.applyTemplateToTarget(annotation.target); + var target = templateSrv.replace(annotation.target); var graphiteQuery = { range: rangeUnparsed, targets: [{ target: target }], @@ -71,7 +71,7 @@ function (angular, _, $, config, kbn, moment) { maxDataPoints: 100 }; - return this.query(filterSrv, graphiteQuery) + return this.query(graphiteQuery) .then(function(result) { var list = []; @@ -95,7 +95,7 @@ function (angular, _, $, config, kbn, moment) { } // Graphite event as annotation else { - var tags = filterSrv.applyTemplateToTarget(annotation.tags); + var tags = templateSrv.replace(annotation.tags); return this.events({ range: rangeUnparsed, tags: tags }) .then(function(results) { var list = []; @@ -166,10 +166,10 @@ function (angular, _, $, config, kbn, moment) { return date.unix(); }; - GraphiteDatasource.prototype.metricFindQuery = function(filterSrv, query) { + GraphiteDatasource.prototype.metricFindQuery = function(query) { var interpolated; try { - interpolated = encodeURIComponent(filterSrv.applyTemplateToTarget(query)); + interpolated = encodeURIComponent(templateSrv.replace(query)); } catch(err) { return $q.reject(err); @@ -210,31 +210,62 @@ function (angular, _, $, config, kbn, moment) { return $http(options); }; - GraphiteDatasource.prototype.buildGraphiteParams = function(filterSrv, options) { - var clean_options = []; - var graphite_options = ['target', 'targets', 'from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; + GraphiteDatasource.prototype._seriesRefLetters = [ + '#A', '#B', '#C', '#D', + '#E', '#F', '#G', '#H', + '#I', '#J', '#K', '#L', + '#M', '#N', '#O' + ]; + + GraphiteDatasource.prototype.buildGraphiteParams = function(options) { + var graphite_options = ['from', 'until', 'rawData', 'format', 'maxDataPoints', 'cacheTimeout']; + var clean_options = [], targets = {}; + var target, targetValue, i; + var regex = /(\#[A-Z])/g; + var intervalFormatFixRegex = /'(\d+)m'/gi; if (options.format !== 'png') { options['format'] = 'json'; } - _.each(options, function (value, key) { - if ($.inArray(key, graphite_options) === -1) { - return; + function fixIntervalFormat(match) { + return match.replace('m', 'min').replace('M', 'mon'); + } + + for (i = 0; i < options.targets.length; i++) { + target = options.targets[i]; + if (!target.target) { + continue; } - if (key === "targets") { - _.each(value, function (value) { - if (value.target && !value.hide) { - var targetValue = filterSrv.applyTemplateToTarget(value.target); - clean_options.push("target=" + encodeURIComponent(targetValue)); - } - }, this); + targetValue = templateSrv.replace(target.target); + targetValue = targetValue.replace(intervalFormatFixRegex, fixIntervalFormat); + targets[this._seriesRefLetters[i]] = targetValue; + } + + function nestedSeriesRegexReplacer(match) { + return targets[match]; + } + + for (i = 0; i < options.targets.length; i++) { + target = options.targets[i]; + if (!target.target || target.hide) { + continue; } - else if (value) { + + targetValue = targets[this._seriesRefLetters[i]]; + targetValue = targetValue.replace(regex, nestedSeriesRegexReplacer); + + clean_options.push("target=" + encodeURIComponent(targetValue)); + } + + _.each(options, function (value, key) { + if ($.inArray(key, graphite_options) === -1) { return; } + if (value) { clean_options.push(key + "=" + encodeURIComponent(value)); } - }, this); + }); + return clean_options; }; diff --git a/src/app/services/graphite/lexer.js b/src/app/services/graphite/lexer.js index 05249b9bff9..ee37e6a22ba 100644 --- a/src/app/services/graphite/lexer.js +++ b/src/app/services/graphite/lexer.js @@ -128,6 +128,7 @@ define([ i === 93 || // templateEnd ] i === 63 || // ? i === 37 || // % + i === 35 || // # i >= 97 && i <= 122; // a-z } diff --git a/src/app/services/graphite/parser.js b/src/app/services/graphite/parser.js index c30e32e7d05..c5ee82deb38 100644 --- a/src/app/services/graphite/parser.js +++ b/src/app/services/graphite/parser.js @@ -157,6 +157,7 @@ define([ var param = this.functionCall() || this.numericLiteral() || + this.seriesRefExpression() || this.metricExpression() || this.stringLiteral(); @@ -168,6 +169,24 @@ define([ return [param].concat(this.functionParameters()); }, + seriesRefExpression: function() { + if (!this.match('identifier')) { + return null; + } + + var value = this.tokens[this.index].value; + if (!value.match(/\#[A-Z]/)) { + return null; + } + + var token = this.consumeToken(); + + return { + type: 'series-ref', + value: token.value + }; + }, + numericLiteral: function () { if (!this.match('number')) { return null; diff --git a/src/app/services/influxdb/influxQueryBuilder.js b/src/app/services/influxdb/influxQueryBuilder.js new file mode 100644 index 00000000000..eb5334fd4e1 --- /dev/null +++ b/src/app/services/influxdb/influxQueryBuilder.js @@ -0,0 +1,67 @@ +define([ +], +function () { + 'use strict'; + + function InfluxQueryBuilder(target) { + this.target = target; + } + + var p = InfluxQueryBuilder.prototype; + + p.build = function() { + return this.target.rawQuery ? this._modifyRawQuery() : this._buildQuery(); + }; + + p._buildQuery = function() { + var target = this.target; + var query = 'select '; + var seriesName = target.series; + + if(!seriesName.match('^/.*/')) { + seriesName = '"' + seriesName+ '"'; + } + + if (target.groupby_field) { + query += target.groupby_field + ', '; + } + + query += target.function + '(' + target.column + ')'; + query += ' from ' + seriesName + ' where $timeFilter'; + + if (target.condition) { + query += ' and ' + target.condition; + } + + query += ' group by time($interval)'; + + if (target.groupby_field) { + query += ', ' + target.groupby_field; + this.groupByField = target.groupby_field; + } + + if (target.fill) { + query += ' fill(' + target.fill + ')'; + } + + query += " order asc"; + target.query = query; + + return query; + }; + + p._modifyRawQuery = function () { + var query = this.target.query.replace(";", ""); + + var queryElements = query.split(" "); + var lowerCaseQueryElements = query.toLowerCase().split(" "); + + if (lowerCaseQueryElements[1].indexOf(',') !== -1) { + this.groupByField = lowerCaseQueryElements[1].replace(',', ''); + } + + return queryElements.join(" "); + }; + + return InfluxQueryBuilder; +}); diff --git a/src/app/services/influxdb/influxdbDatasource.js b/src/app/services/influxdb/influxdbDatasource.js index 4dafd9d7388..c076fb2c5e1 100644 --- a/src/app/services/influxdb/influxdbDatasource.js +++ b/src/app/services/influxdb/influxdbDatasource.js @@ -2,14 +2,15 @@ define([ 'angular', 'lodash', 'kbn', - './influxSeries' + './influxSeries', + './influxQueryBuilder' ], -function (angular, _, kbn, InfluxSeries) { +function (angular, _, kbn, InfluxSeries, InfluxQueryBuilder) { 'use strict'; var module = angular.module('grafana.services'); - module.factory('InfluxDatasource', function($q, $http) { + module.factory('InfluxDatasource', function($q, $http, templateSrv) { function InfluxDatasource(datasource) { this.type = 'influxDB'; @@ -18,9 +19,7 @@ function (angular, _, kbn, InfluxSeries) { this.username = datasource.username; this.password = datasource.password; this.name = datasource.name; - this.templateSettings = { - interpolate : /\[\[([\s\S]+?)\]\]/g, - }; + this.basicAuth = datasource.basicAuth; this.saveTemp = _.isUndefined(datasource.save_temp) ? true : datasource.save_temp; this.saveTempTTL = _.isUndefined(datasource.save_temp_ttl) ? '30d' : datasource.save_temp_ttl; @@ -31,89 +30,28 @@ function (angular, _, kbn, InfluxSeries) { this.annotationEditorSrc = 'app/partials/influxdb/annotation_editor.html'; } - InfluxDatasource.prototype.query = function(filterSrv, options) { - var promises = _.map(options.targets, function(target) { - var query; - var alias = ''; + InfluxDatasource.prototype.query = function(options) { + var timeFilter = getTimeFilter(options); + var promises = _.map(options.targets, function(target) { if (target.hide || !((target.series && target.column) || target.query)) { return []; } - var timeFilter = getTimeFilter(options); - var groupByField; + // build query + var queryBuilder = new InfluxQueryBuilder(target); + var query = queryBuilder.build(); - if (target.rawQuery) { - query = target.query; - query = query.replace(";", ""); - var queryElements = query.split(" "); - var lowerCaseQueryElements = query.toLowerCase().split(" "); - var whereIndex = lowerCaseQueryElements.indexOf("where"); - var groupByIndex = lowerCaseQueryElements.indexOf("group"); - var orderIndex = lowerCaseQueryElements.indexOf("order"); + // replace grafana variables + query = query.replace('$timeFilter', timeFilter); + query = query.replace('$interval', (target.interval || options.interval)); - if (lowerCaseQueryElements[1].indexOf(',') !== -1) { - groupByField = lowerCaseQueryElements[1].replace(',', ''); - } + // replace templated variables + query = templateSrv.replace(query); - if (whereIndex !== -1) { - queryElements.splice(whereIndex + 1, 0, timeFilter, "and"); - } - else { - if (groupByIndex !== -1) { - queryElements.splice(groupByIndex, 0, "where", timeFilter); - } - else if (orderIndex !== -1) { - queryElements.splice(orderIndex, 0, "where", timeFilter); - } - else { - queryElements.push("where"); - queryElements.push(timeFilter); - } - } + var alias = target.alias ? templateSrv.replace(target.alias) : ''; - query = queryElements.join(" "); - query = filterSrv.applyTemplateToTarget(query); - } - else { - - var template = "select [[group]][[group_comma]] [[func]]([[column]]) from [[series]] " + - "where [[timeFilter]] [[condition_add]] [[condition_key]] [[condition_op]] [[condition_value]] " + - "group by time([[interval]])[[group_comma]] [[group]] order asc"; - - var templateData = { - series: target.series, - column: target.column, - func: target.function, - timeFilter: timeFilter, - interval: target.interval || options.interval, - condition_add: target.condition_filter ? 'and' : '', - condition_key: target.condition_filter ? target.condition_key : '', - condition_op: target.condition_filter ? target.condition_op : '', - condition_value: target.condition_filter ? target.condition_value : '', - group_comma: target.groupby_field_add && target.groupby_field ? ',' : '', - group: target.groupby_field_add ? target.groupby_field : '', - }; - - if(!templateData.series.match('^/.*/')) { - templateData.series = '"' + templateData.series + '"'; - } - - query = _.template(template, templateData, this.templateSettings); - query = filterSrv.applyTemplateToTarget(query); - - if (target.groupby_field_add) { - groupByField = target.groupby_field; - } - - target.query = query; - } - - if (target.alias) { - alias = filterSrv.applyTemplateToTarget(target.alias); - } - - var handleResponse = _.partial(handleInfluxQueryResponse, alias, groupByField); + var handleResponse = _.partial(handleInfluxQueryResponse, alias, queryBuilder.groupByField); return this._seriesQuery(query).then(handleResponse); }, this); @@ -121,20 +59,25 @@ function (angular, _, kbn, InfluxSeries) { return $q.all(promises).then(function(results) { return { data: _.flatten(results) }; }); - }; - InfluxDatasource.prototype.annotationQuery = function(annotation, filterSrv, rangeUnparsed) { + InfluxDatasource.prototype.annotationQuery = function(annotation, rangeUnparsed) { var timeFilter = getTimeFilter({ range: rangeUnparsed }); - var query = _.template(annotation.query, { timeFilter: timeFilter }, this.templateSettings); + var query = annotation.query.replace('$timeFilter', timeFilter); + query = templateSrv.replace(query); return this._seriesQuery(query).then(function(results) { return new InfluxSeries({ seriesList: results, annotation: annotation }).getAnnotations(); }); }; - InfluxDatasource.prototype.listColumns = function(seriesName) { - return this._seriesQuery('select * from /' + seriesName + '/ limit 1').then(function(data) { + InfluxDatasource.prototype.listColumns = function(seriesName) { + var interpolated = templateSrv.replace(seriesName); + if (interpolated[0] !== '/') { + interpolated = '/' + interpolated + '/'; + } + + return this._seriesQuery('select * from ' + interpolated + ' limit 1').then(function(data) { if (!data) { return []; } @@ -161,10 +104,10 @@ function (angular, _, kbn, InfluxSeries) { }); }; - InfluxDatasource.prototype.metricFindQuery = function (filterSrv, query) { + InfluxDatasource.prototype.metricFindQuery = function (query) { var interpolated; try { - interpolated = filterSrv.applyTemplateToTarget(query); + interpolated = templateSrv.replace(query); } catch (err) { return $q.reject(err); @@ -228,6 +171,11 @@ function (angular, _, kbn, InfluxSeries) { inspect: { type: 'influxdb' }, }; + options.headers = options.headers || {}; + if (_this.basicAuth) { + options.headers.Authorization = 'Basic ' + _this.basicAuth; + } + return $http(options).success(function (data) { deferred.resolve(data); }); @@ -240,34 +188,46 @@ function (angular, _, kbn, InfluxSeries) { var tags = dashboard.tags.join(','); var title = dashboard.title; var temp = dashboard.temp; + var id = kbn.slugifyForUrl(title); if (temp) { delete dashboard.temp; } var data = [{ - name: 'grafana.dashboard_' + btoa(title), - columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard'], - points: [[1000000000000, 1, title, tags, angular.toJson(dashboard)]] + name: 'grafana.dashboard_' + btoa(id), + columns: ['time', 'sequence_number', 'title', 'tags', 'dashboard', 'id'], + points: [[1000000000000, 1, title, tags, angular.toJson(dashboard), id]] }]; if (temp) { - return this._saveDashboardTemp(data, title); + return this._saveDashboardTemp(data, title, id); } else { + var self = this; return this._influxRequest('POST', '/series', data).then(function() { - return { title: title, url: '/dashboard/db/' + title }; + self._removeUnslugifiedDashboard(title, false); + return { title: title, url: '/dashboard/db/' + id }; }, function(err) { throw 'Failed to save dashboard to InfluxDB: ' + err.data; }); } }; - InfluxDatasource.prototype._saveDashboardTemp = function(data, title) { - data[0].name = 'grafana.temp_dashboard_' + btoa(title); + InfluxDatasource.prototype._removeUnslugifiedDashboard = function(id, isTemp) { + var self = this; + self._getDashboardInternal(id, isTemp).then(function(dashboard) { + if (dashboard !== null) { + self.deleteDashboard(id); + } + }); + }; + + InfluxDatasource.prototype._saveDashboardTemp = function(data, title, id) { + data[0].name = 'grafana.temp_dashboard_' + btoa(id); data[0].columns.push('expires'); data[0].points[0].push(this._getTempDashboardExpiresDate()); return this._influxRequest('POST', '/series', data).then(function() { var baseUrl = window.location.href.replace(window.location.hash,''); - var url = baseUrl + "#dashboard/temp/" + title; + var url = baseUrl + "#dashboard/temp/" + id; return { title: title, url: url }; }, function(err) { throw 'Failed to save shared dashboard to InfluxDB: ' + err.data; @@ -294,7 +254,7 @@ function (angular, _, kbn, InfluxSeries) { return expires; }; - InfluxDatasource.prototype.getDashboard = function(id, isTemp) { + InfluxDatasource.prototype._getDashboardInternal = function(id, isTemp) { var queryString = 'select dashboard from "grafana.dashboard_' + btoa(id) + '"'; if (isTemp) { @@ -303,15 +263,34 @@ function (angular, _, kbn, InfluxSeries) { return this._seriesQuery(queryString).then(function(results) { if (!results || !results.length) { - throw "Dashboard not found"; + return null; } var dashCol = _.indexOf(results[0].columns, 'dashboard'); var dashJson = results[0].points[0][dashCol]; return angular.fromJson(dashJson); + }, function() { + return null; + }); + }; + + InfluxDatasource.prototype.getDashboard = function(id, isTemp) { + var self = this; + return this._getDashboardInternal(id, isTemp).then(function(dashboard) { + if (dashboard !== null) { + return dashboard; + } + + // backward compatible load for unslugified ids + var slug = kbn.slugifyForUrl(id); + if (slug !== id) { + return self.getDashboard(slug, isTemp); + } + + throw "Dashboard not found"; }, function(err) { - return "Could not load dashboard, " + err.data; + throw "Could not load dashboard, " + err.data; }); }; @@ -322,12 +301,12 @@ function (angular, _, kbn, InfluxSeries) { } return id; }, function(err) { - return "Could not delete dashboard, " + err.data; + throw "Could not delete dashboard, " + err.data; }); }; InfluxDatasource.prototype.searchDashboards = function(queryString) { - var influxQuery = 'select title, tags from /grafana.dashboard_.*/ where '; + var influxQuery = 'select * from /grafana.dashboard_.*/ where '; var tagsOnly = queryString.indexOf('tags!:') === 0; if (tagsOnly) { @@ -352,15 +331,21 @@ function (angular, _, kbn, InfluxSeries) { return hits; } - var dashCol = _.indexOf(results[0].columns, 'title'); - var tagsCol = _.indexOf(results[0].columns, 'tags'); - for (var i = 0; i < results.length; i++) { + var dashCol = _.indexOf(results[i].columns, 'title'); + var tagsCol = _.indexOf(results[i].columns, 'tags'); + var idCol = _.indexOf(results[i].columns, 'id'); + var hit = { id: results[i].points[0][dashCol], title: results[i].points[0][dashCol], tags: results[i].points[0][tagsCol].split(",") }; + + if (idCol !== -1) { + hit.id = results[i].points[0][idCol]; + } + hit.tags = hit.tags[0] ? hit.tags : []; hits.dashboards.push(hit); } diff --git a/src/app/services/opentsdb/opentsdbDatasource.js b/src/app/services/opentsdb/opentsdbDatasource.js index 68eb55acc7f..f6a36c6f035 100644 --- a/src/app/services/opentsdb/opentsdbDatasource.js +++ b/src/app/services/opentsdb/opentsdbDatasource.js @@ -19,7 +19,7 @@ function (angular, _, kbn) { } // Called once per panel (graph) - OpenTSDBDatasource.prototype.query = function(filterSrv, options) { + OpenTSDBDatasource.prototype.query = function(options) { var start = convertToTSDBTime(options.range.from); var end = convertToTSDBTime(options.range.to); var queries = _.compact(_.map(options.targets, convertTargetToQuery)); diff --git a/src/app/services/panelMove.js b/src/app/services/panelMove.js index 5dcd5c028c2..aa32400e907 100644 --- a/src/app/services/panelMove.js +++ b/src/app/services/panelMove.js @@ -69,8 +69,14 @@ function (angular, _) { }; return { - create: function(dashboard) { - return new PanelMoveSrv(dashboard); + init: function(dashboard, scope) { + var panelMove = new PanelMoveSrv(dashboard); + + scope.panelMoveDrop = panelMove.onDrop; + scope.panelMoveStart = panelMove.onStart; + scope.panelMoveStop = panelMove.onStop; + scope.panelMoveOver = panelMove.onOver; + scope.panelMoveOut = panelMove.onOut; } }; diff --git a/src/app/services/panelSrv.js b/src/app/services/panelSrv.js index 3c362818f2d..7d664d94b6c 100644 --- a/src/app/services/panelSrv.js +++ b/src/app/services/panelSrv.js @@ -9,9 +9,8 @@ function (angular, _) { module.service('panelSrv', function($rootScope, $timeout, datasourceSrv) { this.init = function($scope) { - if (!$scope.panel.span) { - $scope.panel.span = 12; - } + if (!$scope.panel.span) { $scope.panel.span = 12; } + if (!$scope.panel.title) { $scope.panel.title = 'No title'; } var menu = [ { @@ -52,6 +51,13 @@ function (angular, _) { ], condition: true }, + { + text: 'Advanced', + submenu: [ + { text: 'Panel JSON', click: 'editPanelJson()' }, + ], + condition: true + }, { text: 'Remove', click: 'remove_panel_from_row(row, panel)', @@ -62,6 +68,10 @@ function (angular, _) { $scope.inspector = {}; $scope.panelMeta.menu = _.where(menu, { condition: true }); + $scope.editPanelJson = function() { + $scope.emitAppEvent('show-json-editor', { object: $scope.panel, updateHandler: $scope.replacePanel }); + }; + $scope.updateColumnSpan = function(span) { $scope.panel.span = span; @@ -84,7 +94,7 @@ function (angular, _) { $scope.datasource = datasourceSrv.get(datasource); if (!$scope.datasource) { - $scope.panel.error = "Cannot find datasource " + datasource; + $scope.panelMeta.error = "Cannot find datasource " + datasource; return; } }; @@ -111,7 +121,6 @@ function (angular, _) { $scope.datasources = datasourceSrv.getMetricSources(); $scope.setDatasource($scope.panel.datasource); - $scope.dashboardViewState.registerPanel($scope); if ($scope.get_data) { @@ -119,7 +128,7 @@ function (angular, _) { $scope.get_data = function() { if ($scope.otherPanelInFullscreenMode()) { return; } - delete $scope.panel.error; + delete $scope.panelMeta.error; $scope.panelMeta.loading = true; panel_get_data(); @@ -129,13 +138,6 @@ function (angular, _) { $scope.get_data(); } } - - if ($rootScope.profilingEnabled) { - $rootScope.performance.panelsInitialized++; - if ($rootScope.performance.panelsInitialized === $scope.dashboard.rows.length) { - $rootScope.performance.allPanelsInitialized = new Date().getTime(); - } - } }; }); diff --git a/src/app/services/templateSrv.js b/src/app/services/templateSrv.js new file mode 100644 index 00000000000..c201147becf --- /dev/null +++ b/src/app/services/templateSrv.js @@ -0,0 +1,93 @@ +define([ + 'angular', + 'lodash', +], +function (angular, _) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('templateSrv', function() { + var self = this; + + this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g; + this._values = {}; + this._texts = {}; + this._grafanaVariables = {}; + + this.init = function(variables) { + this.variables = variables; + this.updateTemplateData(); + }; + + this.updateTemplateData = function() { + this._values = {}; + this._texts = {}; + + _.each(this.variables, function(variable) { + if (!variable.current || !variable.current.value) { return; } + + this._values[variable.name] = variable.current.value; + this._texts[variable.name] = variable.current.text; + }, this); + }; + + this.setGrafanaVariable = function (name, value) { + this._grafanaVariables[name] = value; + }; + + this.variableExists = function(expression) { + this._regex.lastIndex = 0; + var match = this._regex.exec(expression); + return match && (self._values[match[1] || match[2]] !== void 0); + }; + + this.containsVariable = function(str, variableName) { + return str.indexOf('$' + variableName) !== -1 || str.indexOf('[[' + variableName + ']]') !== -1; + }; + + this.highlightVariablesAsHtml = function(str) { + if (!str || !_.isString(str)) { return str; } + + this._regex.lastIndex = 0; + return str.replace(this._regex, function(match, g1, g2) { + if (self._values[g1 || g2]) { + return '' + match + ''; + } + return match; + }); + }; + + this.replace = function(target) { + if (!target) { return; } + + var value; + this._regex.lastIndex = 0; + + return target.replace(this._regex, function(match, g1, g2) { + value = self._values[g1 || g2]; + if (!value) { return match; } + + return self._grafanaVariables[value] || value; + }); + }; + + this.replaceWithText = function(target) { + if (!target) { return; } + + var value; + var text; + this._regex.lastIndex = 0; + + return target.replace(this._regex, function(match, g1, g2) { + value = self._values[g1 || g2]; + text = self._texts[g1 || g2]; + if (!value) { return match; } + + return self._grafanaVariables[value] || text; + }); + }; + + }); + +}); diff --git a/src/app/services/templateValuesSrv.js b/src/app/services/templateValuesSrv.js new file mode 100644 index 00000000000..9b80c336f4b --- /dev/null +++ b/src/app/services/templateValuesSrv.js @@ -0,0 +1,171 @@ +define([ + 'angular', + 'lodash', + 'kbn', +], +function (angular, _, kbn) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('templateValuesSrv', function($q, $rootScope, datasourceSrv, $routeParams, templateSrv, timeSrv) { + var self = this; + + $rootScope.onAppEvent('time-range-changed', function() { + var variable = _.findWhere(self.variables, { type: 'interval' }); + if (variable) { + self.updateAutoInterval(variable); + } + }); + + this.init = function(dashboard, viewstate) { + this.variables = dashboard.templating.list; + this.viewstate = viewstate; + templateSrv.init(this.variables); + + for (var i = 0; i < this.variables.length; i++) { + var variable = this.variables[i]; + var urlValue = viewstate.state['var-' + variable.name]; + if (urlValue !== void 0) { + var option = _.findWhere(variable.options, { text: urlValue }); + option = option || { text: urlValue, value: urlValue }; + this.setVariableValue(variable, option, true); + } + else if (variable.refresh) { + this.updateOptions(variable); + } + else if (variable.type === 'interval') { + this.updateAutoInterval(variable); + } + } + }; + + this.updateAutoInterval = function(variable) { + if (!variable.auto) { return; } + + // add auto option if missing + if (variable.options[0].text !== 'auto') { + variable.options.unshift({ text: 'auto', value: '$__auto_interval' }); + } + + var interval = kbn.calculateInterval(timeSrv.timeRange(), variable.auto_count); + templateSrv.setGrafanaVariable('$__auto_interval', interval); + }; + + this.setVariableValue = function(variable, option, recursive) { + variable.current = option; + + templateSrv.updateTemplateData(); + + return this.updateOptionsInChildVariables(variable) + .then(function() { + if (!recursive) { + $rootScope.$broadcast('refresh'); + } + }); + }; + + this.updateOptionsInChildVariables = function(updatedVariable) { + var promises = _.map(self.variables, function(otherVariable) { + if (otherVariable === updatedVariable) { + return; + } + if (templateSrv.containsVariable(otherVariable.query, updatedVariable.name)) { + return self.updateOptions(otherVariable); + } + }); + + return $q.all(promises); + }; + + this._updateNonQueryVariable = function(variable) { + // extract options in comma seperated string + variable.options = _.map(variable.query.split(/[\s,]+/), function(text) { + return { text: text, value: text }; + }); + + if (variable.type === 'interval') { + self.updateAutoInterval(variable); + } + }; + + this.updateOptions = function(variable) { + if (variable.type !== 'query') { + self._updateNonQueryVariable(variable); + self.setVariableValue(variable, variable.options[0]); + return $q.when([]); + } + + var datasource = datasourceSrv.get(variable.datasource); + return datasource.metricFindQuery(variable.query) + .then(function (results) { + variable.options = self.metricNamesToVariableValues(variable, results); + + if (variable.includeAll) { + self.addAllOption(variable); + } + + // if parameter has current value + // if it exists in options array keep value + if (variable.current) { + var currentOption = _.findWhere(variable.options, { text: variable.current.text }); + if (currentOption) { + return self.setVariableValue(variable, currentOption, true); + } + } + + return self.setVariableValue(variable, variable.options[0], true); + }); + }; + + this.metricNamesToVariableValues = function(variable, metricNames) { + var regex, options, i, matches; + options = {}; // use object hash to remove duplicates + + if (variable.regex) { + regex = kbn.stringToJsRegex(templateSrv.replace(variable.regex)); + } + + for (i = 0; i < metricNames.length; i++) { + var value = metricNames[i].text; + + if (regex) { + matches = regex.exec(value); + if (!matches) { continue; } + if (matches.length > 1) { + value = matches[1]; + } + } + + options[value] = value; + } + + return _.map(_.keys(options), function(key) { + return { text: key, value: key }; + }); + }; + + this.addAllOption = function(variable) { + var allValue = ''; + switch(variable.allFormat) { + case 'wildcard': + allValue = '*'; + break; + case 'regex wildcard': + allValue = '.*'; + break; + case 'regex values': + allValue = '(' + _.pluck(variable.options, 'text').join('|') + ')'; + break; + default: + allValue = '{'; + allValue += _.pluck(variable.options, 'text').join(','); + allValue += '}'; + } + + variable.options.unshift({text: 'All', value: allValue}); + }; + + }); + +}); diff --git a/src/app/services/timeSrv.js b/src/app/services/timeSrv.js new file mode 100644 index 00000000000..1ee9ecdf925 --- /dev/null +++ b/src/app/services/timeSrv.js @@ -0,0 +1,121 @@ +define([ + 'angular', + 'lodash', + 'config', + 'kbn', + 'moment' +], function (angular, _, config, kbn, moment) { + 'use strict'; + + var module = angular.module('grafana.services'); + + module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) { + var self = this; + + this.init = function(dashboard) { + timer.cancel_all(); + + this.dashboard = dashboard; + this.time = dashboard.time; + + this._initTimeFromUrl(); + + if(this.dashboard.refresh) { + this.set_interval(this.dashboard.refresh); + } + }; + + this._parseUrlParam = function(value) { + if (value.indexOf('now') !== -1) { + return value; + } + if (value.length === 8) { + return moment.utc(value, 'YYYYMMDD').toDate(); + } + if (value.length === 15) { + return moment.utc(value, 'YYYYMMDDTHHmmss').toDate(); + } + var epoch = parseInt(value); + if (!_.isNaN(epoch)) { + return new Date(epoch); + } + + return null; + }; + + this._initTimeFromUrl = function() { + if ($routeParams.from) { + this.time.from = this._parseUrlParam($routeParams.from) || this.time.from; + } + if ($routeParams.to) { + this.time.to = this._parseUrlParam($routeParams.to) || this.time.to; + } + }; + + this.set_interval = function (interval) { + this.dashboard.refresh = interval; + if (interval) { + var _i = kbn.interval_to_ms(interval); + this.start_scheduled_refresh(_i); + } else { + this.cancel_scheduled_refresh(); + } + }; + + this.refreshDashboard = function() { + $rootScope.$broadcast('refresh'); + }; + + this.start_scheduled_refresh = function (after_ms) { + self.cancel_scheduled_refresh(); + self.refresh_timer = timer.register($timeout(function () { + self.start_scheduled_refresh(after_ms); + self.refreshDashboard(); + }, after_ms)); + }; + + this.cancel_scheduled_refresh = function () { + timer.cancel(this.refresh_timer); + }; + + this.setTime = function(time) { + _.extend(this.time, time); + + // disable refresh if we have an absolute time + if (time.to !== 'now') { + this.old_refresh = this.dashboard.refresh; + this.set_interval(false); + } + else if (this.old_refresh && this.old_refresh !== this.dashboard.refresh) { + this.set_interval(this.old_refresh); + this.old_refresh = null; + } + + $rootScope.emitAppEvent('time-range-changed', this.time); + $timeout(this.refreshDashboard, 0); + }; + + this.timeRange = function(parse) { + var _t = this.time; + if(_.isUndefined(_t) || _.isUndefined(_t.from)) { + return false; + } + if(parse === false) { + return { + from: _t.from, + to: _t.to + }; + } else { + var _from = _t.from; + var _to = _t.to || new Date(); + + return { + from: kbn.parseDate(_from), + to: kbn.parseDate(_to) + }; + } + }; + + }); + +}); diff --git a/src/app/services/unsavedChangesSrv.js b/src/app/services/unsavedChangesSrv.js index c10b78b6bf3..9b8d2eea198 100644 --- a/src/app/services/unsavedChangesSrv.js +++ b/src/app/services/unsavedChangesSrv.js @@ -81,9 +81,16 @@ function(angular, _, config) { // ignore timespan changes current.time = original.time = {}; - current.refresh = original.refresh; + // ignore template variable values + _.each(current.templating.list, function(value, index) { + value.current = null; + value.options = null; + original.templating.list[index].current = null; + original.templating.list[index].options = null; + }); + var currentTimepicker = _.findWhere(current.nav, { type: 'timepicker' }); var originalTimepicker = _.findWhere(original.nav, { type: 'timepicker' }); diff --git a/src/config.sample.js b/src/config.sample.js index c813ac824fe..6046a753d5a 100644 --- a/src/config.sample.js +++ b/src/config.sample.js @@ -78,7 +78,7 @@ function (Settings) { max_results: 20 }, - // default start dashboard + // default home dashboard default_route: '/dashboard/file/default.json', // set to false to disable unsaved changes warning @@ -94,6 +94,9 @@ function (Settings) { password: '' }, + // Change window title prefix from 'Grafana - ' + window_title_prefix: 'Grafana - ', + // Add your own custom pannels plugins: { // list of plugin panels diff --git a/src/css/less/bootswatch.dark.less b/src/css/less/bootswatch.dark.less index e7c9a4a7604..ca155a8e18a 100644 --- a/src/css/less/bootswatch.dark.less +++ b/src/css/less/bootswatch.dark.less @@ -56,7 +56,7 @@ hr { } .brand { - padding: 15px 20px 15px; + padding: 0px 15px; color: @grayLighter; font-weight: normal; text-shadow: none; @@ -212,7 +212,7 @@ div.subnav { .nav-tabs { - border-bottom: 1px solid @grayDark; + border-bottom: 1px solid @fullEditBorder; & > li > a { .border-radius(0); @@ -221,8 +221,9 @@ div.subnav { li > a:hover, li.active > a, li.active > a:hover { - border-color: transparent; - background-color: @blue; + border-color: transparent; + background-color: transparent; + border-bottom: 2px solid @blue; color: @white; } @@ -362,7 +363,7 @@ div.subnav { background-image: none; .box-shadow(none); border: none; - .border-radius(0); + .border-radius(2px); text-shadow: none; &.disabled { @@ -544,16 +545,16 @@ a:hover { .modal { .border-radius(1px); border-top: solid 1px lighten(@grayDark, 5%); - background-color: @grayDark; + background-color: @grafanaPanelBackground; } .modal-header { - border-bottom: 1px solid @grayDark; + border-bottom: 1px solid @grafanaPanelBackground; } .modal-footer { - background-color: @grayDark; - border-top: 1px solid @grayDark; + background-color: @grafanaPanelBackground; + border-top: 1px solid @grafanaPanelBackground; .border-radius(0 0 0px 0px); .box-shadow(none); } diff --git a/src/css/less/bootswatch.light.less b/src/css/less/bootswatch.light.less index ac45149221d..5675d605cbd 100644 --- a/src/css/less/bootswatch.light.less +++ b/src/css/less/bootswatch.light.less @@ -159,8 +159,9 @@ div.subnav { li.active > a, li.active > a:hover { border-color: transparent; - background-color: @blue; - color: @white; + background-color: transparent; + border-bottom: 2px solid @blue; + color: @blue } li.disabled > a { diff --git a/src/css/less/console.less b/src/css/less/console.less index 166314d19df..de413b301a6 100644 --- a/src/css/less/console.less +++ b/src/css/less/console.less @@ -8,8 +8,6 @@ } .grafana-console-header { - background: @fullEditTabsBackground; - border-top: @fullEditTabsBorder; padding: 2px 5px; } diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index f47e1a3156f..08608cb0e46 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -1,8 +1,11 @@ +@import "p_pro.less"; @import "submenu.less"; @import "graph.less"; @import "console.less"; @import "bootstrap-tagsinput.less"; -@import "p_pro.less"; +@import "tables_lists.less"; +@import "search.less"; +@import "panel.less"; .hide-controls { padding: 0; @@ -33,79 +36,17 @@ } } -// Search - -.grafana-search-panel { - .search-field-wrapper { - padding: 6px 10px; - input { - width: 100%; - } - button { - margin: 0 2px 0 0; - } - > span { - display: block; - overflow: hidden; - padding-right: 25px; - } - } -} - -.search-results-container { - max-height: 600px; - overflow: auto; +.logo-icon { + width: 24px; + padding: 13px 11px 0 0; display: block; - .search-result-item a { - } - - .search-result-item:hover, .search-result-item.selected { - .search-result-link, .icon { - color: @grafanaListHighlight; - } - .search-result-link .label { - background-color: @blue; - } - } - - - .search-result-link { - color: @grafanaListMainLinkColor; - .icon { - padding-right: 10px; - color: @grafanaListHighlightContrast; - } - } - - .search-result-item:nth-child(odd) { - background-color: @grafanaListAccent; - } - - .search-result-item { - padding: 6px 10px; - white-space: nowrap; - border-top: 1px solid @grafanaListBorderTop; - border-bottom: 1px solid @grafanaListBorderBottom; - } - - .search-result-tags { - float: right; - } - - .search-result-actions { - float: right; - padding-left: 10px; - } + float: left; } -.search-tagview-switch { - position: absolute; - top: 15px; - right: 266px; - color: darken(@linkColor, 30%); - &.active { - color: @linkColor; - } +.page-title { + padding: 15px 0; + display: block; + float: left; } .row-button { @@ -160,7 +101,7 @@ .panel-fullscreen { z-index: 100; - display: block !important; + display: block; position: fixed; left: 0px; right: 0px; @@ -176,49 +117,16 @@ } } -.dashboard-fullscreen .container-fluid.main { - height: 0px; - width: 0px; - position: fixed; - right: -10000px; +.dashboard-fullscreen { + .row-control-inner { + display: none; + } } .histogram-chart { position:relative; } -.panel-full-edit-tabs { - margin-top: 30px; - min-height: 250px; - margin-left: -10px; - margin-right: -10px; - background-color: @fullEditBackground; - border-top: 1px solid @fullEditBorder; - - .tabs { - .nav-tabs { - margin: 0; - background: @fullEditTabsBackground; - border-top: 1px solid @fullEditTabsBorder; - } - - .tab-content { - display: none; - } - } - .tab-content { - overflow: visible; - padding: 15px; - } - - .nav-tabs > li > a { - line-height: 15px; - padding-top: 6px; - padding-bottom: 6px; - font-size: 0.8rem; - } -} - .grafana-target:last-child { border-bottom: 1px solid @grafanaTargetBorder; } @@ -241,7 +149,6 @@ list-style: none; margin: 0; margin-right: 90px; - margin-left: 30px; >li { float: left; } @@ -249,9 +156,6 @@ .grafana-metric-options { margin-top: 35px; - .grafana-segment-list { - margin-left: 0; - } } // fix for fixed positioned panel & scrolling @@ -281,6 +185,23 @@ &a:hover { background: @grafanaTargetFuncBackground; } + + &.template-param-name { + border-right: none; + padding-right: 3px; + } + &.annotation-segment { + padding: 8px 15px; + } + +} + +.grafana-target-segment-icon { + i { + width: 15px; + text-align: center; + display: inline-block; + } } .grafana-target-function { @@ -306,15 +227,7 @@ input[type=text].grafana-function-param-input { padding: 0; } -.grafana-target-controls-left { - list-style: none; - float: left; - width: 30px; - margin: 0px; -} - .grafana-target-controls { - width: 120px; float: right; list-style: none; margin: 0; @@ -325,10 +238,13 @@ input[type=text].grafana-function-param-input { white-space: nowrap; } - a { - padding: 8px 7px; + .icon { position: relative; top: 8px; + } + + a { + padding: 8px 7px; color: @grafanaTargetColor; font-size: 16px; @@ -350,6 +266,7 @@ input[type=text].grafana-target-text-input { float: left; color: @grafanaTargetColor; border-radius: 0; + border-right: 1px solid @grafanaTargetSegmentBorder; } input[type=text].grafana-target-segment-input { @@ -402,7 +319,6 @@ select.grafana-target-segment-input { } } - .scrollable { max-height: 300px; overflow: auto; @@ -432,23 +348,22 @@ select.grafana-target-segment-input { ::-webkit-scrollbar-button:horizontal:increment:active { background-image: none; } ::-webkit-scrollbar-button:vertical:decrement:active { background-image: none; } ::-webkit-scrollbar-button:vertical:increment:active {background-image: none; } - -::-webkit-scrollbar-track-piece { background-color: grayDark; } +::-webkit-scrollbar-track-piece { background-color: transparent; } ::-webkit-scrollbar-thumb:vertical { height: 50px; - background: -webkit-gradient(linear, left top, right top, color-stop(0%, #3a3a3a), color-stop(100%, #222222)); - border: 1px solid #0d0d0d; - border-top: 1px solid #666666; - border-left: 1px solid #666666; + background: -webkit-gradient(linear, left top, right top, color-stop(0%, @scrollbarBackground), color-stop(100%, @scrollbarBackground2)); + border: 1px solid @scrollbarBorder; + border-top: 1px solid @scrollbarBorder; + border-left: 1px solid @scrollbarBorder; } ::-webkit-scrollbar-thumb:horizontal { width: 50px; - background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #3a3a3a), color-stop(100%, #222222)); - border: 1px solid #1f1f1f; - border-top: 1px solid #666666; - border-left: 1px solid #666666; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, @scrollbarBackground), color-stop(100%, @scrollbarBackground2)); + border: 1px solid @scrollbarBorder; + border-top: 1px solid @scrollbarBorder; + border-left: 1px solid @scrollbarBorder; } @@ -504,12 +419,6 @@ select.grafana-target-segment-input { padding: 10px; } -.grafana-version-footer { - padding-top: 15px; - text-align: left; -} - - .metrics-editor-help:hover { .hide { display: block; @@ -536,3 +445,79 @@ select.grafana-target-segment-input { max-width: 400px; } +.dashboard-edit-view { + padding: 20px; + background-color: @grafanaPanelBackground; + position: relative; +} + +.dashboard-editor-body { + padding: 20px 10px; + min-height: 100px; +} + +.dashboard-editor-footer { + overflow: hidden; +} + +.dashboard-editor-header { + overflow: hidden; + .tabs { + float: left; + } + .nav { + margin: 0; + } +} + +.dashboard-editor-title { + border-bottom: 1px solid @fullEditBorder; + padding-right: 20px; + float: left; + color: @linkColor; + font-size: 20px; + font-weight: normal; + line-height: 38px; + margin: 0; + .icon { + padding: 0 8px 0 5px; + color: @textColor; + } +} + +.grafana-version-info { + position: absolute; + bottom: 2px; + left: 3px; + font-size: 80%; + color: darken(@gray, 25%); + a { color: darken(@gray, 25%); } +} + +.template-variable { + color: @variable; +} + +.grafana-info-box:before { + content: "\f05a"; + font-family:'FontAwesome'; + position: absolute; + top: -8px; + left: -8px; + font-size: 20px; + color: @blue; +} + +.grafana-info-box { + position: relative; + padding: 5px 15px; + background-color: @grafanaTargetBackground; + border: 1px solid @grafanaTargetBorder; + h5 { + margin-top: 5px; + } +} + +.grafana-tip { + padding-left: 5px; +} diff --git a/src/css/less/graph.less b/src/css/less/graph.less index 8f049a44ae5..1bd424999ca 100644 --- a/src/css/less/graph.less +++ b/src/css/less/graph.less @@ -5,8 +5,6 @@ .graph-legend { margin: 0 20px; text-align: center; - position: relative; - top: 2px; .popover-content { padding: 0; @@ -45,7 +43,7 @@ .graph-legend-series { padding-left: 10px; - padding-top: 2px; + padding-top: 6px; } .graph-legend-value { diff --git a/src/css/less/overrides.less b/src/css/less/overrides.less index 7615f35d6f8..58e72ef43fa 100644 --- a/src/css/less/overrides.less +++ b/src/css/less/overrides.less @@ -14,7 +14,7 @@ padding-right: 0px; } -.container.grafana-container { +.main-view-container { padding: 5px 10px; width: 100%; box-sizing: border-box; @@ -65,74 +65,6 @@ code, pre { background-color: @grayLighter; } -.panel { - display: inline-table; - vertical-align: top; -} - -.panel-container { - padding: 0px 0px 0px 0px; - background: @grafanaPanelBackground; - margin: 5px; -} - -.panel-content { - padding: 0px 10px 5px 10px; -} - -.panel-title { - border: 0px; - font-weight: bold; -} - -.panel-loading { - position:absolute; - top: 0px; - right: 4px; - z-index: 800; -} - -.panel div.panel-extra div.panel-extra-container { - margin-right: -10px; - margin-top: 3px; - text-align: center; - ul { - text-align: left; - } -} - -.panel div.panel-extra { - font-size: 0.9em; - margin-bottom: 0px; -} - -.panel div.panel-extra .extra { - float:right !important; -} - -.panel-error { - color: @white; - //padding: 5px 10px 0px 10px; - position: absolute; - left: 5px; - padding: 0px 17px 6px 5px; - top: 0; - i { - position: relative; - top: -2px; - } -} -.panel-error-arrow { - width: 0; - height: 0; - position: absolute; - border-left: 31px solid transparent; - border-right: 30px solid transparent; - border-bottom: 27px solid @grafanaPanelBackground; - left: 0; - bottom: 0; -} - div.editor-row { vertical-align: top; } @@ -142,11 +74,13 @@ div.editor-row div.section { vertical-align: top; display: inline-block; } + div.editor-option { vertical-align: top; display: inline-block; margin-right: 10px; } + div.editor-option label { display: block; } @@ -266,9 +200,9 @@ form input.ng-invalid { left:-34px; position: absolute; z-index: 100; - transition: .25s left; - transition-delay: .25s; - -webkit-transition-delay: .25s; + transition: .10s left; + transition-delay: .10s; + -webkit-transition-delay: .10s; } .row-open:hover { @@ -467,7 +401,6 @@ div.flot-text { /************************* * Right Positions *************************/ - .popover { &.rightTop .arrow { top: 10%; @@ -583,17 +516,30 @@ div.flot-text { } } +// typeahead max height +.typeahead { + max-height: 300px; + overflow-y: auto; +} // Labels & Badges - .label-tag { background-color: @purple; color: darken(@white, 5%); + border-radius: 2px; + text-shadow: none; + font-size: 13px; + padding: 4px 6px; + .icon-tag { + position: relative; + top: 1px; + padding-right: 4px; + } } .label-tag:hover { + opacity: 0.85; background-color: darken(@purple, 10%); - color: @white; } .annotation-editor-table { @@ -603,7 +549,6 @@ div.flot-text { } // Top menu - .save-dashboard-dropdown { padding: 10px; li>a { @@ -632,3 +577,4 @@ code, pre { background-color: @grafanaPanelBackground; color: @textColor; } + diff --git a/src/css/less/panel.less b/src/css/less/panel.less new file mode 100644 index 00000000000..19e5a077872 --- /dev/null +++ b/src/css/less/panel.less @@ -0,0 +1,71 @@ +.panel { + display: inline-block; + float: left; + vertical-align: top; +} + +.panel-container { + padding: 0px 0px 0px 0px; + background: @grafanaPanelBackground; + margin: 5px; + position: relative; +} + +.panel-content { + padding: 0px 10px 5px 10px; +} + +.panel-title { + border: 0px; + font-weight: bold; + position: relative; +} + +.panel-loading { + position:absolute; + top: 0px; + right: 4px; + z-index: 800; +} + +.panel div.panel-extra div.panel-extra-container { + margin-right: -10px; + margin-top: 3px; + text-align: center; + ul { + text-align: left; + } +} + +.panel div.panel-extra { + font-size: 0.9em; + margin-bottom: 0px; +} + +.panel div.panel-extra .extra { + float:right !important; +} + +.panel-error { + color: @white; + position: absolute; + left: 0; + padding: 0px 17px 6px 5px; + top: 0; + i { + position: relative; + top: -2px; + } +} + +.panel-error-arrow { + width: 0; + height: 0; + position: absolute; + border-left: 31px solid transparent; + border-right: 30px solid transparent; + border-bottom: 27px solid @grafanaPanelBackground; + left: 0; + bottom: 0; +} + diff --git a/src/css/less/search.less b/src/css/less/search.less new file mode 100644 index 00000000000..11a03fd0f4c --- /dev/null +++ b/src/css/less/search.less @@ -0,0 +1,83 @@ +// Search +.grafana-search-panel { + .search-field-wrapper { + padding: 6px 10px; + input { + width: 100%; + } + button { + margin: 0 4px 0 0; + } + > span { + display: block; + overflow: hidden; + padding-right: 25px; + } + } +} + +.search-results-container { + height: 500px; + overflow: auto; + display: block; + line-height: 28px; + + .search-result-item:hover, .search-result-item.selected { + .search-result-link, .search-result-link > .icon { + color: @grafanaListHighlight; + } + } + + .selected { + .search-result-tag { + opacity: 0.70; + color: white; + } + } + + .search-result-link { + color: @grafanaListMainLinkColor; + .icon { + padding-right: 10px; + color: @grafanaListHighlightContrast; + } + } + + .search-result-item:nth-child(odd) { + background-color: @grafanaListAccent; + } + + .search-result-item { + padding: 0px 10px; + white-space: nowrap; + border-bottom: 1px solid @grafanaListBorderBottom; + border-top: 1px solid @grafanaListBorderTop; + border-left: 1px solid @grafanaListBorderBottom; + } + + .search-result-tags { + float: right; + .label-tag { + margin-left: 6px; + font-size: 11px; + padding: 2px 6px; + } + } + + .search-result-actions { + float: right; + padding-left: 20px; + } +} + +.search-tagview-switch { + position: absolute; + top: 6px; + right: 24px; + color: darken(@linkColor, 30%); + &.active { + color: @linkColor; + } +} + + diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 514ccf6e29b..8e5c23f7a29 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -1,118 +1,10 @@ -.submenu-controls { - background: @submenuBackground; - font-size: inherit; - label { - margin: 0; - padding-right: 4px; - display: inline; - } - input[type=checkbox] { - margin: 0; - } -} - .submenu-controls-visible:not(.hide-controls) { .panel-fullscreen { - top: 82px; + top: 91px; } } -.submenu-panel { - padding: 0 4px 0 8px; - border-right: 1px solid @submenuBorder; - float: left; -} - -.submenu-panel:first-child { - padding-left: 17px; -} - -.submenu-panel-title { - float: left; - text-transform: uppercase; - padding: 4px 10px 3px 0; -} - -.submenu-panel-wrapper { - float: left; -} - -.submenu-toggle { - padding: 4px 0 3px 8px; - float: left; - .annotation-color-icon { - position: relative; - top: 2px; - } -} - -.submenu-toggle:first-child { - padding-left: 0; -} - -.submenu-control-edit { - padding: 4px 4px 3px 8px; - float: right; - border-left: 1px solid @submenuBorder; - margin-left: 8px; -} - .annotation-disabled, .annotation-disabled a { color: darken(@textColor, 25%); } - -// Filter submenu -.filtering-container { - float: left; -} - -.filtering-container label { - float: left; -} - -.filtering-container input[type=checkbox] { - margin: 0; -} - -.filter-panel-filter { - display:inline-block; - vertical-align: top; - padding: 4px 10px 3px 10px; - border-right: 1px solid @submenuBorder; -} - -.filter-panel-filter:first-child { - padding-left: 0; -} - -.filter-panel-filter ul { - margin-bottom: 0px; -} - -.filter-deselected { - opacity: 0.5; -} - -.filtering-container .filter-action { - float:right; - padding-right: 2px; - margin-bottom: 0px !important; - margin-left: 0px; - margin-top: 4px; -} - -.add-filter-action { - padding: 3px 5px 0px 5px; - position: relative; - top: 4px; -} - -.filter-mandate { - text-decoration: underline; - cursor: pointer; -} - -.filter-apply { - float:right; -} \ No newline at end of file diff --git a/src/css/less/tables_lists.less b/src/css/less/tables_lists.less new file mode 100644 index 00000000000..faa9069ffb8 --- /dev/null +++ b/src/css/less/tables_lists.less @@ -0,0 +1,55 @@ +.grafana-options-table { + width: 100%; + + tr:nth-child(odd) td { + background-color: @grafanaListAccent; + } + + td { + padding: 5px 10px; + white-space: nowrap; + border-bottom: 1px solid @grafanaListBorderBottom; + } + + tr:first-child { + td { + border-top: 1px solid @grafanaListBorderBottom; + } + } + + td:first-child { + border-left: 1px solid @grafanaListBorderBottom; + } + + td:last-child { + border-right: 1px solid @grafanaListBorderBottom; + } +} + +.max-width { + overflow: hidden; + text-overflow: ellipsis; + -o-text-overflow: ellipsis; + white-space: nowrap; +} + +.grafana-options-list { + list-style: none; + margin: 0; + max-width: 450px; + + li:nth-child(odd) { + background-color: @grafanaListAccent; + } + + li { + float: left; + margin: 2px; + padding: 5px 10px; + border: 1px solid @grafanaListBorderBottom; + border: 1px solid @grafanaListBorderBottom; + } + li:first-child { + border: 1px solid @grafanaListBorderBottom; + } +} diff --git a/src/css/less/variables.dark.less b/src/css/less/variables.dark.less index 3397ceed020..608dfb609b0 100644 --- a/src/css/less/variables.dark.less +++ b/src/css/less/variables.dark.less @@ -6,7 +6,7 @@ // ------------------------- @black: #000; @gray: #bbb; -@grayDark: #303030; +@grayDark: #262626; @grayDarker: #1f1f1f; @grayLight: #ADAFAE; @@ -24,20 +24,14 @@ @orange: #FF8800; @pink: #FF4444; @purple: #9933CC; +@variable: #32D1DF; // grafana Variables // ------------------------- @grafanaPanelBackground: @grayDarker; -// Submenu -@submenuBackground: #292929; -@submenuBorder: #202020; - // Tabs -@fullEditTabsBackground: @grayDark; -@fullEditBorder: @black; -@fullEditBackground: transparent; -@fullEditTabsBorder: #555; +@fullEditBorder: #555; // Graphite Target Editor @grafanaTargetBorder: @black; @@ -46,8 +40,8 @@ @grafanaTargetColorHide: darken(#c8c8c8, 25%); @grafanaTargetSegmentBorder: #050505; -@grafanaTargetFuncBackground: #444; -@grafanaTargetFuncHightlight: #555; +@grafanaTargetFuncBackground: #333; +@grafanaTargetFuncHightlight: #444; // Scaffolding // ------------------------- @@ -95,13 +89,18 @@ // Lists @grafanaListBackground: transparent; -@grafanaListAccent: #232323; -@grafanaListBorderTop: #3E3E3E; -@grafanaListBorderBottom: #1c1919; +@grafanaListAccent: lighten(@grayDarker, 2%); +@grafanaListBorderTop: @grayDark; +@grafanaListBorderBottom: @black; @grafanaListHighlight: @blue; @grafanaListHighlightContrast: #4F4F4F; @grafanaListMainLinkColor: @linkColor; +// Scrollbars +@scrollbarBackground: #3a3a3a; +@scrollbarBackground2: #3a3a3a; +@scrollbarBorder: black; + // Tables // ------------------------- @tableBackground: transparent; // overall background-color @@ -134,12 +133,10 @@ @btnInverseBackgroundHighlight: darken(@black, 5%); - - // Forms // ------------------------- -@inputBackground: lighten(@grayDark,10%); -@inputBorder: lighten(@grayDark,20%); +@inputBackground: lighten(@grayDark,5%); +@inputBorder: lighten(@grayDark,5%); @inputBorderRadius: @baseBorderRadius; @inputDisabledBackground: #555; @formActionsBackground: transparent; diff --git a/src/css/less/variables.light.less b/src/css/less/variables.light.less index 562114a9e54..c3453fdd92b 100644 --- a/src/css/less/variables.light.less +++ b/src/css/less/variables.light.less @@ -28,6 +28,7 @@ @orange: #FF7518; @pink: #E671B8; @purple: #9954BB; +@variable: #32D1DF; // grafana Variables // ------------------------- @@ -38,10 +39,7 @@ @submenuBorder: @white; // Tabs -@fullEditTabsBackground: @white; -@fullEditBorder: @white; -@fullEditBackground: @navbarBackground; -@fullEditTabsBorder: @white; +@fullEditBorder: @grayLighter; // Graphite Target Editor @grafanaTargetBorder: @submenuBackground; @@ -61,8 +59,8 @@ // Links // ------------------------- -@linkColor: @blue; -@linkColorHover: darken(@linkColor, 10%); +@linkColor: @textColor; +@linkColorHover: @blue; // Typography @@ -99,7 +97,7 @@ // Lists @grafanaListBackground: transparent; -@grafanaListAccent: #f9f9f9; +@grafanaListAccent: @grayLighter; @grafanaListBorderTop: #eee; @grafanaListBorderBottom: #efefef; @grafanaListHighlight: @blue; @@ -114,6 +112,11 @@ @tableBackgroundHover: #E8F8FD; // for hover @tableBorder: #ddd; // table and cell border +// Scrollbars +@scrollbarBackground: @grayLighter; +@scrollbarBackground2: @grayLighter; +@scrollbarBorder: @grayLight; + // Buttons // ------------------------- @btnBackground: @grayLighter; @@ -189,7 +192,7 @@ // Input placeholder text color // ------------------------- -@placeholderText: @gray; +@placeholderText: @grayLight; // Hr border color @@ -219,7 +222,7 @@ @navbarText: #666; @navbarLinkColor: #666; -@navbarLinkColorHover: #333; +@navbarLinkColorHover: @blue; @navbarLinkColorActive: #555; @navbarLinkBackgroundHover: transparent; @navbarLinkBackgroundActive: darken(@navbarBackground, 6.5%); diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 00000000000..23174fc9623 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/img/fav16.png b/src/img/fav16.png new file mode 100644 index 00000000000..827cd972e4b Binary files /dev/null and b/src/img/fav16.png differ diff --git a/src/img/fav32.png b/src/img/fav32.png new file mode 100644 index 00000000000..827cd972e4b Binary files /dev/null and b/src/img/fav32.png differ diff --git a/src/img/fav_dark_16.png b/src/img/fav_dark_16.png new file mode 100644 index 00000000000..2f66c42db40 Binary files /dev/null and b/src/img/fav_dark_16.png differ diff --git a/src/img/fav_dark_32.png b/src/img/fav_dark_32.png new file mode 100644 index 00000000000..9248832f671 Binary files /dev/null and b/src/img/fav_dark_32.png differ diff --git a/src/index.html b/src/index.html index 63e837abfcd..bf3a2179363 100644 --- a/src/index.html +++ b/src/index.html @@ -5,9 +5,11 @@ + Grafana + diff --git a/src/test/mocks/dashboard-mock.js b/src/test/mocks/dashboard-mock.js index 7ec0e5e9997..6367093bd36 100644 --- a/src/test/mocks/dashboard-mock.js +++ b/src/test/mocks/dashboard-mock.js @@ -5,9 +5,6 @@ define([], return { create: function() { return { - emit_refresh: function() {}, - set_interval: function(value) { this.refresh = value; }, - title: "", tags: [], style: "dark", @@ -18,11 +15,11 @@ define([], rows: [], pulldowns: [ { type: 'templating' }, { type: 'annotations' } ], nav: [ { type: 'timepicker' } ], - time: {}, + time: {from: '1h', to: 'now'}, templating: { list: [] }, - refresh: true + refresh: '10s', }; } }; diff --git a/src/test/specs/dashboardSrv-specs.js b/src/test/specs/dashboardSrv-specs.js index a2c7c1772f6..f916279db98 100644 --- a/src/test/specs/dashboardSrv-specs.js +++ b/src/test/specs/dashboardSrv-specs.js @@ -18,7 +18,6 @@ define([ it('should have default properties', function() { expect(model.rows.length).to.be(0); expect(model.nav.length).to.be(1); - expect(model.pulldowns.length).to.be(2); }); }); @@ -91,6 +90,17 @@ define([ beforeEach(inject(function(dashboardSrv) { model = dashboardSrv.create({ services: { filter: { time: { from: 'now-1d', to: 'now'}, list: [1] }}, + pulldowns: [ + { + type: 'filtering', + enable: true + }, + { + type: 'annotations', + enable: true, + annotations: [{name: 'old'}] + } + ], rows: [ { panels: [ @@ -140,8 +150,14 @@ define([ expect(graph.seriesOverrides[0].yaxis).to.be(2); }); + it('should move pulldowns to new schema', function() { + expect(model.templating.enable).to.be(true); + expect(model.annotations.enable).to.be(true); + expect(model.annotations.list[0].name).to.be('old'); + }); + it('dashboard schema version should be set to latest', function() { - expect(model.version).to.be(4); + expect(model.version).to.be(6); }); }); diff --git a/src/test/specs/dashboardViewStateSrv-specs.js b/src/test/specs/dashboardViewStateSrv-specs.js index 92e444d55d0..82ccfe51363 100644 --- a/src/test/specs/dashboardViewStateSrv-specs.js +++ b/src/test/specs/dashboardViewStateSrv-specs.js @@ -20,6 +20,7 @@ define([ viewState.update(updateState); expect(location.search()).to.eql(updateState); expect(viewState.fullscreen).to.be(true); + expect(viewState.state.fullscreen).to.be(true); }); }); @@ -29,6 +30,7 @@ define([ viewState.update({fullscreen: false}); expect(location.search()).to.eql({}); expect(viewState.fullscreen).to.be(false); + expect(viewState.state.fullscreen).to.be(false); }); }); diff --git a/src/test/specs/filterSrv-specs.js b/src/test/specs/filterSrv-specs.js deleted file mode 100644 index 69a497a66cb..00000000000 --- a/src/test/specs/filterSrv-specs.js +++ /dev/null @@ -1,87 +0,0 @@ -define([ - 'mocks/dashboard-mock', - 'lodash', - 'services/filterSrv' -], function(dashboardMock, _) { - 'use strict'; - - describe('filterSrv', function() { - var _filterSrv; - var _dashboard; - - beforeEach(module('grafana.services')); - beforeEach(module(function() { - _dashboard = dashboardMock.create(); - })); - - beforeEach(inject(function(filterSrv) { - _filterSrv = filterSrv; - })); - - beforeEach(function() { - _filterSrv.init(_dashboard); - }); - - describe('init', function() { - beforeEach(function() { - _filterSrv.addTemplateParameter({ name: 'test', current: { value: 'oogle' } }); - }); - - it('should initialize template data', function() { - var target = _filterSrv.applyTemplateToTarget('this.[[test]].filters'); - expect(target).to.be('this.oogle.filters'); - }); - }); - - describe('updateTemplateData', function() { - beforeEach(function() { - _filterSrv.addTemplateParameter({ - name: 'test', - value: 'muuu', - current: { value: 'muuuu' } - }); - - _filterSrv.updateTemplateData(); - }); - it('should set current value and update template data', function() { - var target = _filterSrv.applyTemplateToTarget('this.[[test]].filters'); - expect(target).to.be('this.muuuu.filters'); - }); - }); - - describe('timeRange', function() { - it('should return unparsed when parse is false', function() { - _filterSrv.setTime({from: 'now', to: 'now-1h' }); - var time = _filterSrv.timeRange(false); - expect(time.from).to.be('now'); - expect(time.to).to.be('now-1h'); - }); - - it('should return parsed when parse is true', function() { - _filterSrv.setTime({from: 'now', to: 'now-1h' }); - var time = _filterSrv.timeRange(true); - expect(_.isDate(time.from)).to.be(true); - expect(_.isDate(time.to)).to.be(true); - }); - }); - - describe('setTime', function() { - it('should return disable refresh for absolute times', function() { - _dashboard.refresh = true; - - _filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' }); - expect(_dashboard.refresh).to.be(false); - }); - - it('should restore refresh after relative time range is set', function() { - _dashboard.refresh = true; - _filterSrv.setTime({from: '2011-01-01', to: '2015-01-01' }); - expect(_dashboard.refresh).to.be(false); - _filterSrv.setTime({from: '2011-01-01', to: 'now' }); - expect(_dashboard.refresh).to.be(true); - }); - }); - - }); - -}); diff --git a/src/test/specs/gfunc-specs.js b/src/test/specs/gfunc-specs.js index cf547b52c35..e0714d8376f 100644 --- a/src/test/specs/gfunc-specs.js +++ b/src/test/specs/gfunc-specs.js @@ -9,9 +9,8 @@ define([ var func = gfunc.createFuncInstance('sumSeries'); expect(func).to.be.ok(); expect(func.def.name).to.equal('sumSeries'); - expect(func.def.params.length).to.equal(0); - expect(func.def.defaultParams.length).to.equal(0); - expect(func.def.defaultParams.length).to.equal(0); + expect(func.def.params.length).to.equal(5); + expect(func.def.defaultParams.length).to.equal(1); }); it('should return func instance with shortName', function() { @@ -42,11 +41,16 @@ define([ expect(func.render('hello.metric')).to.equal("sumSeries(hello.metric)"); }); + it('should include default params if options enable it', function() { + var func = gfunc.createFuncInstance('scaleToSeconds', { withDefaultParams: true }); + expect(func.render('hello')).to.equal("scaleToSeconds(hello, 1)"); + }); + it('should handle metric param and int param and string param', function() { var func = gfunc.createFuncInstance('groupByNode'); func.params[0] = 5; func.params[1] = 'avg'; - expect(func.render('hello.metric')).to.equal("groupByNode(hello.metric,5,'avg')"); + expect(func.render('hello.metric')).to.equal("groupByNode(hello.metric, 5, 'avg')"); }); it('should handle function with no metric param', function() { @@ -55,6 +59,12 @@ define([ expect(func.render(undefined)).to.equal("randomWalk('test')"); }); + it('should handle function multiple series params', function() { + var func = gfunc.createFuncInstance('asPercent'); + func.params[0] = '#B'; + expect(func.render('#A')).to.equal("asPercent(#A, #B)"); + }); + }); describe('when requesting function categories', function() { @@ -66,7 +76,7 @@ define([ describe('when updating func param', function() { it('should update param value and update text representation', function() { - var func = gfunc.createFuncInstance('summarize'); + var func = gfunc.createFuncInstance('summarize', { withDefaultParams: true }); func.updateParam('1h', 0); expect(func.params[0]).to.be('1h'); expect(func.text).to.be('summarize(1h, sum)'); @@ -75,7 +85,7 @@ define([ it('should parse numbers as float', function() { var func = gfunc.createFuncInstance('scale'); func.updateParam('0.001', 0); - expect(func.params[0]).to.be(0.001); + expect(func.params[0]).to.be('0.001'); }); }); @@ -83,14 +93,14 @@ define([ it('should update value and text', function() { var func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('1', 0); - expect(func.params[0]).to.be(1); + expect(func.params[0]).to.be('1'); }); it('should slit text and put value in second param', function() { var func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); - expect(func.params[0]).to.be(4); - expect(func.params[1]).to.be(-5); + expect(func.params[0]).to.be('4'); + expect(func.params[1]).to.be('-5'); expect(func.text).to.be('aliasByNode(4, -5)'); }); @@ -98,7 +108,7 @@ define([ var func = gfunc.createFuncInstance('aliasByNode'); func.updateParam('4,-5', 0); func.updateParam('', 1); - expect(func.params[0]).to.be(4); + expect(func.params[0]).to.be('4'); expect(func.params[1]).to.be(undefined); expect(func.text).to.be('aliasByNode(4)'); }); diff --git a/src/test/specs/grafanaGraph-specs.js b/src/test/specs/grafanaGraph-specs.js index c7a27740f4d..1b86ee9073d 100644 --- a/src/test/specs/grafanaGraph-specs.js +++ b/src/test/specs/grafanaGraph-specs.js @@ -15,6 +15,9 @@ define([ describe(desc, function() { var ctx = {}; ctx.setup = function (setupFunc) { + beforeEach(module(function($provide) { + $provide.value("timeSrv", new helpers.TimeSrvStub()); + })); beforeEach(inject(function($rootScope, $compile) { var scope = $rootScope.$new(); var element = angular.element("
    "); diff --git a/src/test/specs/graph-ctrl-specs.js b/src/test/specs/graph-ctrl-specs.js index c01353feb62..e1ddcbc571e 100644 --- a/src/test/specs/graph-ctrl-specs.js +++ b/src/test/specs/graph-ctrl-specs.js @@ -36,8 +36,8 @@ define([ var data = ctx.scope.render.getCall(0).args[0]; expect(data.length).to.be(2); }); - }); + }); }); diff --git a/src/test/specs/graphiteDatasource-specs.js b/src/test/specs/graphiteDatasource-specs.js new file mode 100644 index 00000000000..99270867266 --- /dev/null +++ b/src/test/specs/graphiteDatasource-specs.js @@ -0,0 +1,103 @@ +define([ + './helpers', + 'services/graphite/graphiteDatasource' +], function(helpers) { + 'use strict'; + + describe('graphiteDatasource', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase()); + beforeEach(ctx.createService('GraphiteDatasource')); + beforeEach(function() { + ctx.ds = new ctx.service({ url: [''] }); + }); + + describe('When querying influxdb with one target using query editor target spec', function() { + var query = { + range: { from: 'now-1h', to: 'now' }, + targets: [{ target: 'prod1.count' }, {target: 'prod2.count'}], + maxDataPoints: 500, + }; + + var response = [{ target: 'prod1.count', points: [[10, 1], [12,1]], }]; + var results; + var request; + + beforeEach(function() { + + ctx.$httpBackend.expectPOST('/render', function(body) { request = body; return true; }) + .respond(response); + + ctx.ds.query(query).then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + }); + + it('should generate the correct query', function() { + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + + it('should query correctly', function() { + var params = request.split('&'); + expect(params).to.contain('target=prod1.count'); + expect(params).to.contain('target=prod2.count'); + expect(params).to.contain('from=-1h'); + expect(params).to.contain('until=now'); + }); + + it('should exclude undefined params', function() { + var params = request.split('&'); + expect(params).to.not.contain('cacheTimeout=undefined'); + }); + + it('should return series list', function() { + expect(results.data.length).to.be(1); + expect(results.data[0].target).to.be('prod1.count'); + }); + + }); + + describe('building graphite params', function() { + + it('should uri escape targets', function() { + var results = ctx.ds.buildGraphiteParams({ + targets: [{target: 'prod1.{test,test2}'}, {target: 'prod2.count'}] + }); + expect(results).to.contain('target=prod1.%7Btest%2Ctest2%7D'); + }); + + it('should replace target placeholder', function() { + var results = ctx.ds.buildGraphiteParams({ + targets: [{target: 'series1'}, {target: 'series2'}, {target: 'asPercent(#A,#B)'}] + }); + expect(results[2]).to.be('target=asPercent(series1%2Cseries2)'); + }); + + it('should fix wrong minute interval parameters', function() { + var results = ctx.ds.buildGraphiteParams({ + targets: [{target: "summarize(prod.25m.count, '25m', 'sum')" }] + }); + expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.25m.count, '25min', 'sum')")); + }); + + it('should fix wrong month interval parameters', function() { + var results = ctx.ds.buildGraphiteParams({ + targets: [{target: "summarize(prod.5M.count, '5M', 'sum')" }] + }); + expect(results[0]).to.be('target=' + encodeURIComponent("summarize(prod.5M.count, '5mon', 'sum')")); + }); + + it('should ignore empty targets', function() { + var results = ctx.ds.buildGraphiteParams({ + targets: [{target: 'series1'}, {target: ''}] + }); + expect(results.length).to.be(2); + }); + + }); + + }); + +}); + diff --git a/src/test/specs/graphiteTargetCtrl-specs.js b/src/test/specs/graphiteTargetCtrl-specs.js index dfe51960fc5..27be0d92b26 100644 --- a/src/test/specs/graphiteTargetCtrl-specs.js +++ b/src/test/specs/graphiteTargetCtrl-specs.js @@ -28,7 +28,7 @@ define([ }); it('should validate metric key exists', function() { - expect(ctx.scope.datasource.metricFindQuery.getCall(0).args[1]).to.be('test.prod.*'); + expect(ctx.scope.datasource.metricFindQuery.getCall(0).args[0]).to.be('test.prod.*'); }); it('should delete last segment if no metrics are found', function() { @@ -56,7 +56,7 @@ define([ }); it('should update target', function() { - expect(ctx.scope.target.target).to.be('aliasByNode(test.prod.*.count,2)'); + expect(ctx.scope.target.target).to.be('aliasByNode(test.prod.*.count, 2)'); }); it('should call get_data', function() { @@ -64,6 +64,78 @@ define([ }); }); + describe('when adding function before any metric segment', function() { + beforeEach(function() { + ctx.scope.target.target = ''; + ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: true}])); + ctx.scope.init(); + ctx.scope.$digest(); + + ctx.scope.$parent = { get_data: sinon.spy() }; + ctx.scope.addFunction(gfunc.getFuncDef('asPercent')); + }); + + it('should add function and remove select metric link', function() { + expect(ctx.scope.segments.length).to.be(0); + }); + }); + + describe('when initalizing target without metric expression and only function', function() { + beforeEach(function() { + ctx.scope.target.target = 'asPercent(#A, #B)'; + ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.scope.init(); + ctx.scope.$digest(); + ctx.scope.$parent = { get_data: sinon.spy() }; + }); + + it('should not add select metric segment', function() { + expect(ctx.scope.segments.length).to.be(0); + }); + + it('should add both series refs as params', function() { + expect(ctx.scope.functions[0].params.length).to.be(2); + }); + + }); + + describe('when initializing a target with single param func using variable', function() { + beforeEach(function() { + ctx.scope.target.target = 'movingAverage(prod.count, $var)'; + ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.scope.init(); + ctx.scope.$digest(); + ctx.scope.$parent = { get_data: sinon.spy() }; + }); + + it('should add 2 segments', function() { + expect(ctx.scope.segments.length).to.be(2); + }); + + it('should add function param', function() { + expect(ctx.scope.functions[0].params.length).to.be(1); + }); + + }); + + describe('when initalizing target without metric expression and function with series-ref', function() { + beforeEach(function() { + ctx.scope.target.target = 'asPercent(metric.node.count, #A)'; + ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([])); + ctx.scope.init(); + ctx.scope.$digest(); + ctx.scope.$parent = { get_data: sinon.spy() }; + }); + + it('should add segments', function() { + expect(ctx.scope.segments.length).to.be(3); + }); + + it('should have correct func params', function() { + expect(ctx.scope.functions[0].params.length).to.be(1); + }); + }); + describe('targetChanged', function() { beforeEach(function() { ctx.scope.datasource.metricFindQuery.returns(ctx.$q.when([{expandable: false}])); @@ -76,7 +148,7 @@ define([ }); it('should rebuld target after expression model', function() { - expect(ctx.scope.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*,1),2)'); + expect(ctx.scope.target.target).to.be('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)'); }); it('should call get_data', function() { diff --git a/src/test/specs/helpers.js b/src/test/specs/helpers.js index 4f2eb7ea680..81f0df7be7e 100644 --- a/src/test/specs/helpers.js +++ b/src/test/specs/helpers.js @@ -1,6 +1,7 @@ define([ - 'kbn' -], function(kbn) { + 'kbn', + 'lodash' +], function(kbn, _) { 'use strict'; function ControllerTestContext() { @@ -8,6 +9,8 @@ define([ this.datasource = {}; this.annotationsSrv = {}; + this.timeSrv = new TimeSrvStub(); + this.templateSrv = new TemplateSrvStub(); this.datasourceSrv = { getMetricSources: function() {}, get: function() { return self.datasource; } @@ -17,6 +20,8 @@ define([ return module(function($provide) { $provide.value('datasourceSrv', self.datasourceSrv); $provide.value('annotationsSrv', self.annotationsSrv); + $provide.value('timeSrv', self.timeSrv); + $provide.value('templateSrv', self.templateSrv); }); }; @@ -25,7 +30,6 @@ define([ self.scope = $rootScope.$new(); self.scope.panel = {}; self.scope.row = { panels:[] }; - self.scope.filter = new FilterSrvStub(); self.scope.dashboard = {}; self.scope.dashboardViewState = new DashboardViewStateStub(); @@ -44,15 +48,30 @@ define([ function ServiceTestContext() { var self = this; + self.templateSrv = new TemplateSrvStub(); + self.timeSrv = new TimeSrvStub(); + self.datasourceSrv = {}; + self.$routeParams = {}; + + this.providePhase = function(mocks) { + return module(function($provide) { + _.each(mocks, function(key) { + $provide.value(key, self[key]); + }); + }); + }; this.createService = function(name) { - return inject([name, '$q', '$rootScope', '$httpBackend', function(InfluxDatasource, $q, $rootScope, $httpBackend) { - self.service = InfluxDatasource; + return inject(function($q, $rootScope, $httpBackend, $injector) { self.$q = $q; self.$rootScope = $rootScope; - self.filterSrv = new FilterSrvStub(); self.$httpBackend = $httpBackend; - }]); + + self.$rootScope.onAppEvent = function() {}; + self.$rootScope.emitAppEvent = function() {}; + + self.service = $injector.get(name); + }); }; } @@ -61,7 +80,7 @@ define([ }; } - function FilterSrvStub() { + function TimeSrvStub() { this.time = { from:'now-1h', to: 'now'}; this.timeRange = function(parse) { if (parse === false) { @@ -73,15 +92,30 @@ define([ }; }; - this.applyTemplateToTarget = function(target) { + this.replace = function(target) { return target; }; } + function TemplateSrvStub() { + this.variables = []; + this.templateSettings = { interpolate : /\[\[([\s\S]+?)\]\]/g }; + this.data = {}; + this.replace = function(text) { + return _.template(text, this.data, this.templateSettings); + }; + this.init = function() {}; + this.updateTemplateData = function() { }; + this.variableExists = function() { return false; }; + this.highlightVariablesAsHtml = function(str) { return str; }; + this.setGrafanaVariable = function(name, value) { + this.data[name] = value; + }; + } return { ControllerTestContext: ControllerTestContext, - FilterSrvStub: FilterSrvStub, + TimeSrvStub: TimeSrvStub, ServiceTestContext: ServiceTestContext }; diff --git a/src/test/specs/influxQueryBuilder-specs.js b/src/test/specs/influxQueryBuilder-specs.js new file mode 100644 index 00000000000..4f18bf2905e --- /dev/null +++ b/src/test/specs/influxQueryBuilder-specs.js @@ -0,0 +1,49 @@ +define([ + 'services/influxdb/influxQueryBuilder' +], function(InfluxQueryBuilder) { + 'use strict'; + + describe('InfluxQueryBuilder', function() { + + describe('series with conditon and group by', function() { + var builder = new InfluxQueryBuilder({ + series: 'google.test', + column: 'value', + function: 'mean', + condition: "code=1", + groupby_field: 'code' + }); + + var query = builder.build(); + + it('should generate correct query', function() { + expect(query).to.be('select code, mean(value) from "google.test" where $timeFilter and code=1 ' + + 'group by time($interval), code order asc'); + }); + + it('should expose groupByFiled', function() { + expect(builder.groupByField).to.be('code'); + }); + + }); + + describe('series with fill and minimum group by time', function() { + var builder = new InfluxQueryBuilder({ + series: 'google.test', + column: 'value', + function: 'mean', + fill: '0', + }); + + var query = builder.build(); + + it('should generate correct query', function() { + expect(query).to.be('select mean(value) from "google.test" where $timeFilter ' + + 'group by time($interval) fill(0) order asc'); + }); + + }); + + }); + +}); diff --git a/src/test/specs/influxdb-datasource-specs.js b/src/test/specs/influxdb-datasource-specs.js index a164c2aec62..3851b0cd898 100644 --- a/src/test/specs/influxdb-datasource-specs.js +++ b/src/test/specs/influxdb-datasource-specs.js @@ -8,15 +8,20 @@ define([ var ctx = new helpers.ServiceTestContext(); beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['templateSrv'])); beforeEach(ctx.createService('InfluxDatasource')); + beforeEach(function() { + ctx.ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' }); + }); describe('When querying influxdb with one target using query editor target spec', function() { var results; - var urlExpected = "/series?p=mupp&q=select++mean(value)+from+%22test%22"+ - "+where++time+%3E+now()+-+1h+++++group+by+time()++order+asc&time_precision=s"; + var urlExpected = "/series?p=mupp&q=select+mean(value)+from+%22test%22"+ + "+where+time+%3E+now()+-+1h+group+by+time(1s)+order+asc&time_precision=s"; var query = { range: { from: 'now-1h', to: 'now' }, - targets: [{ series: 'test', column: 'value', function: 'mean' }] + targets: [{ series: 'test', column: 'value', function: 'mean' }], + interval: '1s' }; var response = [{ @@ -26,10 +31,8 @@ define([ }]; beforeEach(function() { - var ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' }); - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ds.query(ctx.filterSrv, query).then(function(data) { results = data; }); + ctx.ds.query(query).then(function(data) { results = data; }); ctx.$httpBackend.flush(); }); @@ -47,19 +50,41 @@ define([ describe('When querying influxdb with one raw query', function() { var results; var urlExpected = "/series?p=mupp&q=select+value+from+series"+ - "+where+time+%3E+now()+-+1h+and+time+%3E+1&time_precision=s"; + "+where+time+%3E+now()+-+1h&time_precision=s"; var query = { range: { from: 'now-1h', to: 'now' }, - targets: [{ query: "select value from series where time > 1", rawQuery: true }] + targets: [{ query: "select value from series where $timeFilter", rawQuery: true }] }; var response = []; beforeEach(function() { - var ds = new ctx.service({ urls: [''], user: 'test', password: 'mupp' }); - ctx.$httpBackend.expect('GET', urlExpected).respond(response); - ds.query(ctx.filterSrv, query).then(function(data) { results = data; }); + ctx.ds.query(query).then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + }); + + it('should generate the correct query', function() { + ctx.$httpBackend.verifyNoOutstandingExpectation(); + }); + + }); + + describe('When issuing annotation query', function() { + var results; + var urlExpected = "/series?p=mupp&q=select+title+from+events.backend_01"+ + "+where+time+%3E+now()+-+1h&time_precision=s"; + + var range = { from: 'now-1h', to: 'now' }; + var annotation = { query: 'select title from events.$server where $timeFilter' }; + var response = []; + + beforeEach(function() { + ctx.templateSrv.replace = function(str) { + return str.replace('$server', 'backend_01'); + }; + ctx.$httpBackend.expect('GET', urlExpected).respond(response); + ctx.ds.annotationQuery(annotation, range).then(function(data) { results = data; }); ctx.$httpBackend.flush(); }); diff --git a/src/test/specs/kbn-format-specs.js b/src/test/specs/kbn-format-specs.js index 0faa4728f2d..a44475556b0 100644 --- a/src/test/specs/kbn-format-specs.js +++ b/src/test/specs/kbn-format-specs.js @@ -41,7 +41,6 @@ define([ }); describe('nanosecond formatting', function () { - it('should translate 25 to 25 ns', function () { var str = kbn.nanosFormat(25, 2); expect(str).to.be("25 ns"); @@ -68,4 +67,38 @@ define([ }); }); + + describe('calculateInterval', function() { + it('1h 100 resultion', function() { + var range = { from: kbn.parseDate('now-1h'), to: kbn.parseDate('now') }; + var str = kbn.calculateInterval(range, 100, null); + expect(str).to.be('30s'); + }); + + it('10m 1600 resolution', function() { + var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') }; + var str = kbn.calculateInterval(range, 1600, null); + expect(str).to.be('0.1s'); + }); + + it('fixed user interval', function() { + var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') }; + var str = kbn.calculateInterval(range, 1600, '10s'); + expect(str).to.be('10s'); + }); + + it('short time range and user low limit', function() { + var range = { from: kbn.parseDate('now-10m'), to: kbn.parseDate('now') }; + var str = kbn.calculateInterval(range, 1600, '>10s'); + expect(str).to.be('10s'); + }); + + it('large time range and user low limit', function() { + var range = { from: kbn.parseDate('now-14d'), to: kbn.parseDate('now') }; + var str = kbn.calculateInterval(range, 1000, '>10s'); + expect(str).to.be('30m'); + }); + + }); + }); diff --git a/src/test/specs/parser-specs.js b/src/test/specs/parser-specs.js index 802474a5814..33c234ae823 100644 --- a/src/test/specs/parser-specs.js +++ b/src/test/specs/parser-specs.js @@ -156,6 +156,16 @@ define([ expect(rootNode.segments[1].value).to.be('test'); }); + it('series parameters', function() { + var parser = new Parser('asPercent(#A, #B)'); + var rootNode = parser.getAst(); + expect(rootNode.type).to.be('function'); + expect(rootNode.params[0].type).to.be('series-ref'); + expect(rootNode.params[0].value).to.be('#A'); + expect(rootNode.params[1].value).to.be('#B'); + }); + + }); }); diff --git a/src/test/specs/templateSrv-specs.js b/src/test/specs/templateSrv-specs.js new file mode 100644 index 00000000000..f740ef6d544 --- /dev/null +++ b/src/test/specs/templateSrv-specs.js @@ -0,0 +1,114 @@ +define([ + 'mocks/dashboard-mock', + 'lodash', + 'services/templateSrv' +], function(dashboardMock) { + 'use strict'; + + describe('templateSrv', function() { + var _templateSrv; + var _dashboard; + + beforeEach(module('grafana.services')); + beforeEach(module(function() { + _dashboard = dashboardMock.create(); + })); + + beforeEach(inject(function(templateSrv) { + _templateSrv = templateSrv; + })); + + describe('init', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); + }); + + it('should initialize template data', function() { + var target = _templateSrv.replace('this.[[test]].filters'); + expect(target).to.be('this.oogle.filters'); + }); + }); + + describe('can check if variable exists', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); + }); + + it('should return true if exists', function() { + var result = _templateSrv.variableExists('$test'); + expect(result).to.be(true); + }); + }); + + describe('can hightlight variables in string', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'oogle' } }]); + }); + + it('should insert html', function() { + var result = _templateSrv.highlightVariablesAsHtml('$test'); + expect(result).to.be('$test'); + }); + + it('should insert html anywhere in string', function() { + var result = _templateSrv.highlightVariablesAsHtml('this $test ok'); + expect(result).to.be('this $test ok'); + }); + + it('should ignore if variables does not exist', function() { + var result = _templateSrv.highlightVariablesAsHtml('this $google ok'); + expect(result).to.be('this $google ok'); + }); + + }); + + describe('when checking if a string contains a variable', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]); + _templateSrv.updateTemplateData(); + }); + + it('should find it with $var syntax', function() { + var contains = _templateSrv.containsVariable('this.$test.filters', 'test'); + expect(contains).to.be(true); + }); + + it('should find it with [[var]] syntax', function() { + var contains = _templateSrv.containsVariable('this.[[test]].filters', 'test'); + expect(contains).to.be(true); + }); + + }); + + describe('updateTemplateData with simple value', function() { + beforeEach(function() { + _templateSrv.init([{ name: 'test', current: { value: 'muuuu' } }]); + _templateSrv.updateTemplateData(); + }); + + it('should set current value and update template data', function() { + var target = _templateSrv.replace('this.[[test]].filters'); + expect(target).to.be('this.muuuu.filters'); + }); + }); + + describe('replaceWithText', function() { + beforeEach(function() { + _templateSrv.init([ + { name: 'server', current: { value: '{asd,asd2}', text: 'All' } }, + { name: 'period', current: { value: '$__auto_interval', text: 'auto' } } + ]); + _templateSrv.setGrafanaVariable('$__auto_interval', '13m'); + _templateSrv.updateTemplateData(); + }); + + it('should replace with text except for grafanaVariables', function() { + var target = _templateSrv.replaceWithText('Server: $server, period: $period'); + expect(target).to.be('Server: All, period: 13m'); + }); + }); + + + }); + +}); diff --git a/src/test/specs/templateValuesSrv-specs.js b/src/test/specs/templateValuesSrv-specs.js new file mode 100644 index 00000000000..bc0de36e959 --- /dev/null +++ b/src/test/specs/templateValuesSrv-specs.js @@ -0,0 +1,253 @@ +define([ + 'mocks/dashboard-mock', + './helpers', + 'moment', + 'services/templateValuesSrv' +], function(dashboardMock, helpers, moment) { + 'use strict'; + + describe('templateValuesSrv', function() { + var ctx = new helpers.ServiceTestContext(); + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['datasourceSrv', 'timeSrv', 'templateSrv', "$routeParams"])); + beforeEach(ctx.createService('templateValuesSrv')); + + describe('update interval variable options', function() { + var variable = { type: 'interval', query: 'auto,1s,2h,5h,1d', name: 'test' }; + + beforeEach(function() { + ctx.service.updateOptions(variable); + }); + + it('should update options array', function() { + expect(variable.options.length).to.be(5); + expect(variable.options[1].text).to.be('1s'); + expect(variable.options[1].value).to.be('1s'); + }); + }); + + function describeUpdateVariable(desc, fn) { + describe(desc, function() { + var scenario = {}; + scenario.setup = function(setupFn) { + scenario.setupFn = setupFn; + }; + + beforeEach(function() { + scenario.setupFn(); + var ds = {}; + ds.metricFindQuery = sinon.stub().returns(ctx.$q.when(scenario.queryResult)); + ctx.datasourceSrv.get = sinon.stub().returns(ds); + + ctx.service.updateOptions(scenario.variable); + ctx.$rootScope.$digest(); + }); + + fn(scenario); + }); + } + + describeUpdateVariable('interval variable without auto', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' }; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).to.be(4); + expect(scenario.variable.options[0].text).to.be('1s'); + expect(scenario.variable.options[0].value).to.be('1s'); + }); + }); + + describeUpdateVariable('interval variable with auto', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test', auto: true, auto_count: 10 }; + + var range = { + from: moment(new Date()).subtract(7, 'days').toDate(), + to: new Date() + }; + + ctx.timeSrv.timeRange = sinon.stub().returns(range); + ctx.templateSrv.setGrafanaVariable = sinon.spy(); + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).to.be(5); + expect(scenario.variable.options[0].text).to.be('auto'); + expect(scenario.variable.options[0].value).to.be('$__auto_interval'); + }); + + it('should set $__auto_interval', function() { + var call = ctx.templateSrv.setGrafanaVariable.getCall(0); + expect(call.args[0]).to.be('$__auto_interval'); + expect(call.args[1]).to.be('12h'); + }); + }); + + describeUpdateVariable('update custom variable', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'custom', query: 'hej, hop, asd', name: 'test'}; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).to.be(3); + expect(scenario.variable.options[0].text).to.be('hej'); + expect(scenario.variable.options[1].value).to.be('hop'); + }); + + it('should set $__auto_interval', function() { + var call = ctx.templateSrv.setGrafanaVariable.getCall(0); + expect(call.args[0]).to.be('$__auto_interval'); + expect(call.args[1]).to.be('12h'); + }); + }); + + describeUpdateVariable('basic query variable', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; + }); + + it('should update options array', function() { + expect(scenario.variable.options.length).to.be(2); + expect(scenario.variable.options[0].text).to.be('backend1'); + expect(scenario.variable.options[0].value).to.be('backend1'); + expect(scenario.variable.options[1].value).to.be('backend2'); + }); + + it('should select first option as value', function() { + expect(scenario.variable.current.value).to.be('backend1'); + }); + }); + + describeUpdateVariable('and existing value still exists in options', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variable.current = { text: 'backend2'}; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}]; + }); + + it('should keep variable value', function() { + expect(scenario.variable.current.text).to.be('backend2'); + }); + }); + + describeUpdateVariable('and regex pattern exists', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variable.regex = '/apps.*(backend_[0-9]+)/'; + scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}]; + }); + + it('should extract and use match group', function() { + expect(scenario.variable.options[0].value).to.be('backend_01'); + }); + }); + + describeUpdateVariable('and regex pattern exists and no match', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variable.regex = '/apps.*(backendasd[0-9]+)/'; + scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}]; + }); + + it('should not add non matching items', function() { + expect(scenario.variable.options.length).to.be(0); + }); + }); + + describeUpdateVariable('regex pattern without slashes', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variable.regex = 'backend_01'; + scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_02.counters.req'}]; + }); + + it('should return matches options', function() { + expect(scenario.variable.options.length).to.be(1); + }); + }); + + describeUpdateVariable('regex pattern remove duplicates', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test' }; + scenario.variable.regex = 'backend_01'; + scenario.queryResult = [{text: 'apps.backend.backend_01.counters.req'}, {text: 'apps.backend.backend_01.counters.req'}]; + }); + + it('should return matches options', function() { + expect(scenario.variable.options.length).to.be(1); + }); + }); + + describeUpdateVariable('with include All glob syntax', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; + }); + + it('should add All Glob option', function() { + expect(scenario.variable.options[0].value).to.be('{backend1,backend2,backend3}'); + }); + }); + + describeUpdateVariable('with include all wildcard', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; + }); + + it('should add All wildcard option', function() { + expect(scenario.variable.options[0].value).to.be('*'); + }); + }); + + describeUpdateVariable('with include all wildcard', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'regex wildcard' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; + }); + + it('should add All wildcard option', function() { + expect(scenario.variable.options[0].value).to.be('.*'); + }); + }); + + describeUpdateVariable('with include all regex values', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'wildcard' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; + }); + + it('should add All wildcard option', function() { + expect(scenario.variable.options[0].value).to.be('*'); + }); + }); + + describeUpdateVariable('with include all glob no values', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'glob' }; + scenario.queryResult = []; + }); + + it('should add empty glob', function() { + expect(scenario.variable.options[0].value).to.be('{}'); + }); + }); + + describeUpdateVariable('with include all regex all values', function(scenario) { + scenario.setup(function() { + scenario.variable = { type: 'query', query: 'apps.*', name: 'test', includeAll: true, allFormat: 'regex values' }; + scenario.queryResult = [{text: 'backend1'}, {text: 'backend2'}, { text: 'backend3'}]; + }); + + it('should add empty glob', function() { + expect(scenario.variable.options[0].value).to.be('(backend1|backend2|backend3)'); + }); + }); + + }); + +}); diff --git a/src/test/specs/timeSrv-specs.js b/src/test/specs/timeSrv-specs.js new file mode 100644 index 00000000000..1d8a158e27e --- /dev/null +++ b/src/test/specs/timeSrv-specs.js @@ -0,0 +1,96 @@ +define([ + 'mocks/dashboard-mock', + './helpers', + 'lodash', + 'services/timeSrv' +], function(dashboardMock, helpers, _) { + 'use strict'; + + describe('timeSrv', function() { + var ctx = new helpers.ServiceTestContext(); + var _dashboard; + + beforeEach(module('grafana.services')); + beforeEach(ctx.providePhase(['$routeParams'])); + beforeEach(ctx.createService('timeSrv')); + + beforeEach(function() { + _dashboard = dashboardMock.create(); + ctx.service.init(_dashboard); + }); + + describe('timeRange', function() { + it('should return unparsed when parse is false', function() { + ctx.service.setTime({from: 'now', to: 'now-1h' }); + var time = ctx.service.timeRange(false); + expect(time.from).to.be('now'); + expect(time.to).to.be('now-1h'); + }); + + it('should return parsed when parse is true', function() { + ctx.service.setTime({from: 'now', to: 'now-1h' }); + var time = ctx.service.timeRange(true); + expect(_.isDate(time.from)).to.be(true); + expect(_.isDate(time.to)).to.be(true); + }); + }); + + describe('init time from url', function() { + it('should handle relative times', function() { + ctx.$routeParams.from = 'now-2d'; + ctx.$routeParams.to = 'now'; + ctx.service.init(_dashboard); + var time = ctx.service.timeRange(false); + expect(time.from).to.be('now-2d'); + expect(time.to).to.be('now'); + }); + + it('should handle formated dates', function() { + ctx.$routeParams.from = '20140410T052010'; + ctx.$routeParams.to = '20140520T031022'; + ctx.service.init(_dashboard); + var time = ctx.service.timeRange(true); + expect(time.from.getTime()).to.equal(new Date("2014-04-10T05:20:10Z").getTime()); + expect(time.to.getTime()).to.equal(new Date("2014-05-20T03:10:22Z").getTime()); + }); + + it('should handle formated dates without time', function() { + ctx.$routeParams.from = '20140410'; + ctx.$routeParams.to = '20140520'; + ctx.service.init(_dashboard); + var time = ctx.service.timeRange(true); + expect(time.from.getTime()).to.equal(new Date("2014-04-10T00:00:00Z").getTime()); + expect(time.to.getTime()).to.equal(new Date("2014-05-20T00:00:00Z").getTime()); + }); + + it('should handle epochs', function() { + ctx.$routeParams.from = '1410337646373'; + ctx.$routeParams.to = '1410337665699'; + ctx.service.init(_dashboard); + var time = ctx.service.timeRange(true); + expect(time.from.getTime()).to.equal(1410337646373); + expect(time.to.getTime()).to.equal(1410337665699); + }); + + }); + + describe('setTime', function() { + it('should return disable refresh for absolute times', function() { + _dashboard.refresh = false; + + ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); + expect(_dashboard.refresh).to.be(false); + }); + + it('should restore refresh after relative time range is set', function() { + _dashboard.refresh = '10s'; + ctx.service.setTime({from: '2011-01-01', to: '2015-01-01' }); + expect(_dashboard.refresh).to.be(false); + ctx.service.setTime({from: '2011-01-01', to: 'now' }); + expect(_dashboard.refresh).to.be('10s'); + }); + }); + + }); + +}); diff --git a/src/test/test-main.js b/src/test/test-main.js index 6a62eaa6a3f..8d074a2c1c6 100644 --- a/src/test/test-main.js +++ b/src/test/test-main.js @@ -121,14 +121,18 @@ require([ 'specs/timeSeries-specs', 'specs/row-ctrl-specs', 'specs/graphiteTargetCtrl-specs', + 'specs/graphiteDatasource-specs', + 'specs/influxSeries-specs', + 'specs/influxQueryBuilder-specs', 'specs/influxdb-datasource-specs', 'specs/graph-ctrl-specs', 'specs/grafanaGraph-specs', 'specs/seriesOverridesCtrl-specs', - 'specs/filterSrv-specs', + 'specs/timeSrv-specs', + 'specs/templateSrv-specs', + 'specs/templateValuesSrv-specs', 'specs/kbn-format-specs', 'specs/dashboardSrv-specs', - 'specs/influxSeries-specs', 'specs/dashboardViewStateSrv-specs', 'specs/overview-ctrl-specs', ], function () { diff --git a/tasks/options/requirejs.js b/tasks/options/requirejs.js index b3bc4f97909..c1a718c3397 100644 --- a/tasks/options/requirejs.js +++ b/tasks/options/requirejs.js @@ -1,4 +1,6 @@ module.exports = function(config,grunt) { + 'use strict'; + var _c = { build: { options: { @@ -59,12 +61,15 @@ module.exports = function(config,grunt) { 'directives/all', 'jquery.flot.pie', 'angular-dragdrop', + 'controllers/all', + 'routes/all', + 'components/partials', ] } ]; var fs = require('fs'); - var panelPath = config.srcDir+'/app/panels' + var panelPath = config.srcDir+'/app/panels'; // create a module for each directory in src/app/panels/ fs.readdirSync(panelPath).forEach(function (panelName) {