Merge branch 'master' of github.com:torkelo/grafana-private into pro

Conflicts:
	src/css/less/grafana.less
	src/test/test-main.js
This commit is contained in:
Torkel Ödegaard 2014-09-17 09:54:40 +02:00
commit 4b382e0faf
130 changed files with 4600 additions and 2240 deletions

View File

@ -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))

View File

@ -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"
}

View File

@ -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"

View File

@ -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;
});

View File

@ -14,6 +14,7 @@ function (_, crypto) {
*/
var defaults = {
datasources : {},
window_title_prefix : 'Grafana - ',
panels : ['graph', 'text'],
plugins : {},
default_route : '/dashboard/file/default.json',

View File

@ -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;
}

View File

@ -13,5 +13,7 @@ define([
'./playlistCtrl',
'./inspectCtrl',
'./opentsdbTargetCtrl',
'./console-ctrl',
'./annotationsEditorCtrl',
'./templateEditorCtrl',
'./jsonEditorCtrl',
], function () {});

View File

@ -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);
};
});
});

View File

@ -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) {

View File

@ -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;

View File

@ -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));
}
});

View File

@ -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);
});

View File

@ -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);
};
});
});

View File

@ -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() {

View File

@ -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);
};

View File

@ -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);
};
});
});

View File

@ -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();
});
});
});

View File

@ -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);
};
});
});

View File

@ -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": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"http://grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
"content": "<div class=\"text-center\" style=\"padding-top: 15px\">\n<img src=\"//grafana.org/assets/img/logo_transparent_200x75.png\"> \n</div>",
"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": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs#configuration\" target=\"_blank\">Configuration</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/troubleshooting\" target=\"_blank\">Troubleshooting</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/support\" target=\"_blank\">Support</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/intro\" target=\"_blank\">Getting started</a> (Must read!)\n </li>\n </ul>\n </div>\n <div class=\"span6\">\n <ul>\n <li>\n <a href=\"http://grafana.org/docs/features/graphing\" target=\"_blank\">Graphing</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/annotations\" target=\"_blank\">Annotations</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/graphite\" target=\"_blank\">Graphite</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/influxdb\" target=\"_blank\">InfluxDB</a>\n </li>\n <li>\n <a href=\"http://grafana.org/docs/features/opentsdb\" target=\"_blank\">OpenTSDB</a>\n </li>\n </ul>\n </div>\n</div>",
"style": {},
"title": "Documentation Links"
},
{
"error": false,
"id": 3,
"span": 6,
"editable": true,
"type": "text",
"mode": "html",
"content": "<br/>\n\n<div class=\"row-fluid\">\n <div class=\"span12\">\n <ul>\n <li>Ctrl+S saves the current dashboard</li>\n <li>Ctrl+F Opens the dashboard finder</li>\n <li>Ctrl+H Hide/show row controls</li>\n <li>Click and drag graph title to move panel</li>\n <li>Hit Escape to exit graph when in fullscreen or edit mode</li>\n <li>Click the colored icon in the legend to change series color</li>\n <li>Ctrl or Shift + Click legend name to hide other series</li>\n </ul>\n </div>\n</div>\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
}
"version": 5
}

View File

@ -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": [

View File

@ -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);
});
}
}

View File

@ -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;

View File

@ -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
}

View File

@ -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);

View File

@ -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 () {});

View File

@ -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() {

View File

@ -102,7 +102,7 @@ function (angular, $) {
var li = '<li' + (item.submenu && item.submenu.length ? ' class="dropdown-submenu"' : '') + '>' +
'<a tabindex="-1" ng-href="' + (item.href || '') + '"' + (item.click ? ' ng-click="' + item.click + '"' : '') +
(item.target ? ' target="' + item.target + '"' : '') + (item.method ? ' data-method="' + item.method + '"' : '') +
(item.configModal ? ' config-modal="' + item.configModal + '"' : "") +
(item.configModal ? ' dash-editor-link="' + item.configModal + '"' : "") +
'>' + (item.text || '') + '</a>';
if (item.submenu && item.submenu.length) {
@ -131,4 +131,4 @@ function (angular, $) {
}
};
});
});
});

View File

@ -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 = $('<div class="dashboard-edit-view" ng-include="' + src + '"></div>');
elem.append(view);
$compile(elem.contents())(editorScope);
});
}
};
});
});

View File

@ -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++) {

View File

@ -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: '<div> </div>',
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(),
});

View File

@ -18,8 +18,8 @@ function (angular, $) {
'<div class="row-fluid panel-extra">' +
'<div class="panel-extra-container">' +
'<span class="alert-error panel-error small pointer"' +
'config-modal="app/partials/inspector.html" ng-if="panel.error">' +
'<span data-placement="right" bs-tooltip="panel.error">' +
'config-modal="app/partials/inspector.html" ng-if="panelMeta.error">' +
'<span data-placement="right" bs-tooltip="panelMeta.error">' +
'<i class="icon-exclamation-sign"></i><span class="panel-error-arrow"></span>' +
'</span>' +
'</span>' +
@ -40,7 +40,7 @@ function (angular, $) {
'onStop:\'panelMoveStop\''+
'}" ng-model="panel" ' +
'>' +
'{{panel.title || "No title"}}' +
'{{panel.title | interpolateTemplateVars}}' +
'</span>' +
'</span>'+

View File

@ -30,4 +30,4 @@ function (angular) {
}
};
});
});
});

View File

@ -8,7 +8,7 @@ function (angular, _, $) {
angular
.module('grafana.directives')
.directive('graphiteFuncEditor', function($compile) {
.directive('graphiteFuncEditor', function($compile, templateSrv) {
var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
var paramTemplate = '<input type="text" style="display:none"' +
@ -69,12 +69,12 @@ function (angular, _, $) {
function inputBlur(paramIndex) {
/*jshint validthis:true */
var $input = $(this);
var $link = $input.prev();
var newValue = $input.val();
if ($input.val() !== '' || func.def.params[paramIndex].optional) {
$link.text($input.val());
if (newValue !== '' || func.def.params[paramIndex].optional) {
$link.html(templateSrv.highlightVariablesAsHtml(newValue));
func.updateParam($input.val(), paramIndex);
scheduledRelinkIfNeeded();
@ -88,7 +88,6 @@ function (angular, _, $) {
function inputKeyPress(paramIndex, e) {
/*jshint validthis:true */
if(e.which === 13) {
inputBlur.call(this, paramIndex);
}
@ -147,7 +146,7 @@ function (angular, _, $) {
$funcLink.appendTo(elem);
_.each(funcDef.params, function(param, index) {
if (param.optional && !func.params[index]) {
if (param.optional && func.params.length <= index) {
return;
}
@ -155,7 +154,8 @@ function (angular, _, $) {
$('<span>, </span>').appendTo(elem);
}
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + func.params[index] + '</a>');
var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
var $input = $(paramTemplate);
paramCountAtLink++;
@ -239,4 +239,4 @@ function (angular, _, $) {
});
});
});

View File

@ -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 = '<input type="text" data-provide="typeahead" ' +
' class="grafana-target-text-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="grafana-target-segment" tabindex="1" focus-me="segment.focus" ng-bind-html="segment.html"></a>';
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);
}
};
});
});

View File

@ -0,0 +1,82 @@
define([
'angular',
'app',
'lodash',
'jquery',
],
function (angular, app, _, $) {
'use strict';
angular
.module('grafana.directives')
.directive('templateParamSelector', function($compile) {
var inputTemplate = '<input type="text" data-provide="typeahead" ' +
' class="grafana-target-text-input input-medium"' +
' spellcheck="false" style="display:none"></input>';
var buttonTemplate = '<a class="grafana-target-segment tabindex="1">{{variable.current.text}}</a>';
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);
}
};
});
});

View File

@ -11,10 +11,10 @@ function (angular, kbn) {
return {
restrict: 'E',
link: function(scope, elem, attrs) {
var _t = '<i class="icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
var _t = '<i class="grafana-tip icon-'+(attrs.icon||'question-sign')+'" bs-tooltip="\''+
kbn.addslashes(elem.text())+'\'"></i>';
elem.replaceWith($compile(angular.element(_t))(scope));
}
};
});
});
});

View File

@ -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, "<a href=\"$1\" target=\"_blank\">$1</a>");
});
text = t1 || text;
_.each(text.match(r2), function() {
t2 = text.replace(r2, "$1<a href=\"http://$2\" target=\"_blank\">$2</a>");
});
text = t2 || text;
_.each(text.match(r3), function() {
t3 = text.replace(r3, "<a href=\"mailto:$1\">$1</a>");
});
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(/.*\//, '');
}
}
};
});
});
});

View File

@ -1,67 +0,0 @@
<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
<div class="modal-body">
<div class="pull-right editor-title">Annotations</div>
<div class="editor-row">
<table class="table table-striped annotation-editor-table" style="width: 700px">
<thead>
<th width="90%">Name</th>
<th width="1%"></th>
<th width="1%"></th>
<th width="1%"></th>
</thead>
<tr ng-repeat="annotation in panel.annotations">
<td>
<a ng-click="edit(annotation)" bs-tooltip="'Click to edit'">
<i class="icon-cog"></i>
{{annotation.name}}
</a>
</td>
<td><i ng-click="_.move(panel.annotations,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(panel.annotations,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td><i ng-click="panel.annotations = _.without(panel.annotations, annotation)" class="pointer icon-remove"></i></td>
</tr>
</table>
</div>
<div class="editor-row">
<h4 ng-show="currentIsNew">Add Annotation</h4>
<h4 ng-show="!currentIsNew">Edit Annotation</h4>
<div class="editor-option">
<label class="small">Name</label>
<input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
</div>
<div class="editor-option">
<label class="small">Datasource</label>
<select ng-model="currentDatasource" ng-options="f.name for f in datasources" ng-change="setDatasource()"></select>
</div>
<div class="editor-option">
<label class="small">Icon color</label>
<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
</div>
<div class="editor-option">
<label class="small">Icon size</label>
<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
</div>
<div class="editor-option">
<label class="small">Grid line</label>
<input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
</div>
<div class="editor-option">
<label class="small">Line color</label>
<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
</div>
</div>
<div ng-include src="currentDatasource.editorSrc">
</div>
</div>
<div class="modal-footer">
<button ng-show="currentIsNew" type="button" class="btn btn-success" ng-click="add()">Add annotation</button>
<button ng-show="!currentIsNew" type="button" class="btn btn-success" ng-click="update()">Update</button>
<button type="button" class="btn btn-danger" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
</div>
</div>

View File

@ -1,12 +0,0 @@
<div ng-controller='AnnotationsCtrl' ng-init="init()">
<div class="submenu-toggle" ng-repeat="annotation in panel.annotations" ng-class="{'annotation-disabled': !annotation.enable }">
<i class="annotation-color-icon icon-minus"></i>
<a ng-click="hide(annotation)" class="small">{{annotation.name}}</a>
</div>
<div class="submenu-control-edit">
<i class="icon-cog pointer" config-modal="app/panels/annotations/editor.html" bs-tooltip="'Edit annotations'" ></i>
</div>
</div>

View File

@ -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');
};
});
});

View File

@ -1,50 +0,0 @@
<div ng-controller='filtering' ng-init="init()">
<div class='filtering-container'>
<div ng-repeat="filter in filter.templateParameters" class="small filter-panel-filter">
<div>
<i class="filter-action pointer icon-remove" bs-tooltip="'Remove'" ng-click="remove(filter)"></i>
<i class="filter-action pointer icon-edit" ng-hide="filter.editing" bs-tooltip="'Edit'" ng-click="filter.editing = true"></i>
</div>
<div ng-hide="filter.editing" style="margin-right: 45px;">
<ul class="unstyled">
<li ng-if="filter.name" class="dropdown">
{{filter.name}} :
<a class="dropdown-toggle" data-toggle="dropdown">
{{filter.current.text}}
</a>
<ul class="dropdown-menu">
<li ng-repeat="option in filter.options">
<a ng-click="filterOptionSelected(filter, option)">{{option.text}}</a>
</li>
</ul>
</li>
</ul>
</div>
<form ng-show="filter.editing">
<ul class="unstyled">
<li>
<strong>name</strong>:<br/>
<input type='text' ng-model="filter.name">
</li>
<li>
<strong>filter.query</strong>:<br/>
<input type='text' ng-model="filter.query">
</li>
<li>
<label for="includeAll">Include all:</label>
<input id="includeAll" type='checkbox' ng-model="filter.includeAll">
</li>
</ul>
<div>
<input type="submit" value="Update" ng-click="applyFilter(filter)" class="filter-apply btn btn-success btn-mini" bs-tooltip="'Update and refresh'"/>
<button ng-click="filter.editing=undefined" class="filter-apply btn btn-mini" bs-tooltip="'Save without refresh'">Close</button>
</div>
</form>
</div>
<i class="pointer icon-plus-sign add-filter-action" ng-click="add()" bs-tooltip="'Add metric filter / param'" data-placement="right"></i>
</div>
</div>

View File

@ -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.*',
});
};
});
});

View File

@ -21,14 +21,23 @@
<div class="clearfix"></div>
<div class="panel-full-edit-tabs" ng-if="editMode">
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
</div>
</div>
<div style="margin-top: 30px" ng-if="editMode">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-bar-chart"></i>
Graph
</div>
<div class="tab-content" ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in editorTabs" data-title="{{tab}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div ng-repeat="tab in panelMeta.fullEditorTabs" ng-if="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
</div>
</div>

View File

@ -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);
});

View File

@ -27,7 +27,7 @@
<select class="input-mini" ng-model="panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="render()"></select>
</div>
<div class="editor-option">
<label class="small">Null point mode <tip>Define how null values should be drawn</tip></label>
<label class="small">Null point mode<tip>Define how null values should be drawn</tip></label>
<select class="input-medium" ng-model="panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="render()"></select>
</div>
<div class="editor-option">
@ -67,34 +67,30 @@
<div class="section">
<h5>Series specific overrides <tip>Regex match example: /server[0-3]/i </tip></h5>
<div>
<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
<div class="grafana-target-inner-wrapper">
<div class="grafana-target" ng-repeat="override in panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
<div class="grafana-target-inner">
<ul class="grafana-target-controls-left">
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
<i class="icon-remove pointer" ng-click="removeSeriesOverride(override)"></i>
</li>
</ul>
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
alias or regex
</li>
<li>
<input type="text"
ng-model="override.alias"
bs-typeahead="getSeriesNames"
ng-blur="render()"
data-min-length=0 data-items=100
class="input-medium grafana-target-segment-input" >
ng-model="override.alias"
bs-typeahead="getSeriesNames"
ng-blur="render()"
data-min-length=0 data-items=100
class="input-medium grafana-target-segment-input" >
</li>
<li class="grafana-target-segment" ng-repeat="option in currentOverrides">
<i class="pointer icon-remove" ng-click="removeOverride(option)"></i>
{{option.name}}: {{option.value}}
</li>
<li class="dropdown">
<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'">
<a class="dropdown-toggle grafana-target-segment" data-toggle="dropdown" gf-dropdown="overrideMenu" bs-tooltip="'set option to override'" data-placement="right">
<i class="icon-plus"></i>
</a>
</li>
@ -103,7 +99,6 @@
</div>
</div>
</div>
</div>
<button class="btn btn-success" style="margin-top: 20px" ng-click="addSeriesOverride()">Add series override rule</button>
</div>

View File

@ -9,10 +9,9 @@
</div>
<label class=small>Content
<span ng-show="panel.mode == 'html'">(This area uses HTML sanitized via AngularJS's <a href='http://docs.angularjs.org/api/ngSanitize.$sanitize'>$sanitize</a> service)</span>
<span ng-show="panel.mode == 'markdown'">(This area uses <a target="_blank" href="http://en.wikipedia.org/wiki/Markdown">Markdown</a>. HTML is not supported)</span>
</label>
<textarea ng-model="panel.content" rows="6" style="width:95%" ng-change="render()" ng-model-onblur>
<textarea ng-model="panel.content" rows="20" style="width:95%" ng-change="render()" ng-model-onblur>
</textarea>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div ng-controller='text'>
<p ng-bind-html="content">
<p ng-bind-html="content" ng-show="content">
</p>
</div>

View File

@ -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);

View File

@ -1,78 +1,84 @@
<div class="modal-body">
<style>
.timepicker-to-column {
margin-top: 10px;
}
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-calendar-empty"></i>
Custom time range
</div>
</div>
.timepicker-input input {
outline: 0 !important;
border: 0px !important;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
position: relative;
}
<div class="dashboard-editor-body">
<style>
.timepicker-to-column {
margin-top: 10px;
}
.timepicker-input input::-webkit-outer-spin-button,
.timepicker-input input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.timepicker-input input {
outline: 0 !important;
border: 0px !important;
-webkit-box-shadow: 0;
-moz-box-shadow: 0;
box-shadow: 0;
position: relative;
}
input.timepicker-date {
width: 90px;
}
input.timepicker-hms {
width: 20px;
}
input.timepicker-ms {
width: 25px;
}
div.timepicker-now {
float: right;
}
</style>
.timepicker-input input::-webkit-outer-spin-button,
.timepicker-input input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
<div class="timepicker form-horizontal">
<form name="input">
input.timepicker-date {
width: 90px;
}
input.timepicker-hms {
width: 20px;
}
input.timepicker-ms {
width: 25px;
}
div.timepicker-now {
float: right;
}
</style>
<div class="timepicker-from-column">
<label class="small">From</label>
<div class="fake-input timepicker-input">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
</div>
<div class="timepicker form-horizontal">
<form name="input">
<div class="timepicker-to-column">
<div class="timepicker-from-column">
<label class="small">From</label>
<div class="fake-input timepicker-input">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.from.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.from.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.from.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
</div>
<label class="small">To (<a class="link" ng-class="{'strong':tempnow}" ng-click="setNow();tempnow=true">now</a>)</label>
<div class="timepicker-to-column">
<div class="fake-input timepicker-input">
<div ng-hide="tempnow">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
<span type="text" ng-show="tempnow" ng-disabled="tempnow">&nbsp <i class="pointer icon-remove-sign" ng-click="setNow();tempnow=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
</div>
</div>
<label class="small">To (<a class="link" ng-class="{'strong':temptime.now}" ng-click="setNow();temptime.now=true">now</a>)</label>
</form>
<div class="clearfix"></div>
</div>
</div>
<div class="fake-input timepicker-input">
<div ng-hide="temptime.now">
<input class="timepicker-date" type="text" ng-change="validate(temptime)" ng-model="temptime.to.date" data-date-format="yyyy-mm-dd" required bs-datepicker />@
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.hour" required ng-pattern="patterns.hour" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.minute" required ng-pattern="patterns.minute" onClick="this.select();"/>:
<input class="timepicker-hms" type="text" maxlength="2" ng-change="validate(temptime)" ng-model="temptime.to.second" required ng-pattern="patterns.second" onClick="this.select();"/>.
<input class="timepicker-ms" type="text" maxlength="3" ng-change="validate(temptime)" ng-model="temptime.to.millisecond" required ng-pattern="patterns.millisecond" onClick="this.select();"/>
</div>
<span type="text" ng-show="temptime.now" ng-disabled="temptime.now">&nbsp <i class="pointer icon-remove-sign" ng-click="setNow();temptime.now=false;"></i> Right Now <input type="text" name="dummy" style="visibility:hidden" /></span>
</div>
</div>
<div class="modal-footer">
<form name="input" style="margin-bottom:0">
<span class="" ng-hide="input.$valid">Invalid date or range</span>
<button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
<button ng-click="dismiss();" class="btn btn-danger">Cancel</button>
</form>
<div class="clearfix"></div>
</div>
</div>
</form>
</div>
<div class="dashboard-editor-footer">
<form name="input" style="margin-bottom:0">
<span class="" ng-hide="input.$valid">Invalid date or range</span>
<button ng-click="setAbsoluteTimeFilter(validate(temptime));dismiss();" ng-disabled="!input.$valid" class="btn btn-success">Apply</button>
<button ng-click="dismiss();" class="btn btn-success pull-right">Cancel</button>
</form>
</div>

View File

@ -16,8 +16,7 @@
<li class="dropdown">
<a class="dropdown-toggle timepicker-dropdown" data-toggle="dropdown" href="" bs-tooltip="time.tooltip" data-placement="bottom" ng-click="dismiss();">
<span ng-show="filter.time" ng-bind="time.rangeString"></span>
<span ng-hide="filter.time">Time filter</span>
<span ng-bind="time.rangeString"></span>
<span ng-show="dashboard.refresh" class="text-warning">refreshed every {{dashboard.refresh}} </span>
<i class="icon-caret-down"></i>
</a>
@ -33,10 +32,10 @@
<a href="#">Auto-Refresh</a>
<ul class="dropdown-menu">
<li>
<a ng-click="dashboard.set_interval(false)">Off</a>
<a ng-click="timeSrv.set_interval(false)">Off</a>
</li>
<li bindonce ng-repeat="interval in panel.refresh_intervals track by $index">
<a ng-click="dashboard.set_interval(interval)" bo-text="'Every ' + interval"></a>
<a ng-click="timeSrv.set_interval(interval)" bo-text="'Every ' + interval"></a>
</li>
</ul>
</li>
@ -45,7 +44,7 @@
</li>
<li ng-show="!dashboard.refresh" class="grafana-menu-refresh">
<a ng-click="dashboard.emit_refresh()"><i class="icon-refresh"></i></a>
<a ng-click="timeSrv.refreshDashboard()"><i class="icon-refresh"></i></a>
</li>
</ul>

View File

@ -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();

View File

@ -0,0 +1,85 @@
<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-bolt"></i>
Annotations
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['Overview', 'Add', 'Edit']" data-title="{{tab}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div class="editor-row row" ng-if="editor.index == 0">
<div class="span6">
<div ng-if="variables.length === 0">
<em>No annotations defined</em>
</div>
<table class="grafana-options-table">
<tr ng-repeat="annotation in annotations">
<td style="width:90%">
<i class="icon-bolt"></i> &nbsp;
{{annotation.name}}
</td>
<td style="width: 1%">
<a ng-click="edit(annotation)" class="btn btn-success btn-mini">
<i class="icon-edit"></i>
Edit
</a>
</td>
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td style="width: 1%">
<a ng-click="removeAnnotation(annotation)" class="btn btn-danger btn-mini">
<i class="icon-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
<div class="editor-row">
<div class="editor-option">
<label class="small">Name</label>
<input type="text" class="input-medium" ng-model='currentAnnotation.name' placeholder="name"></input>
</div>
<div class="editor-option">
<label class="small">Datasource</label>
<select ng-model="currentAnnotation.datasource" ng-options="f.name as f.name for f in datasources" ng-change="datasourceChanged()"></select>
</div>
<div class="editor-option">
<label class="small">Icon color</label>
<spectrum-picker ng-model="currentAnnotation.iconColor"></spectrum-picker>
</div>
<div class="editor-option">
<label class="small">Icon size</label>
<select class="input-mini" ng-model="currentAnnotation.iconSize" ng-options="f for f in [7,8,9,10,13,15,17,20,25,30]"></select>
</div>
<div class="editor-option">
<label class="small">Grid line</label>
<input type="checkbox" ng-model="currentAnnotation.showLine" ng-checked="currentAnnotation.showLine">
</div>
<div class="editor-option">
<label class="small">Line color</label>
<spectrum-picker ng-model="currentAnnotation.lineColor"></spectrum-picker>
</div>
</div>
<div ng-include src="currentDatasource.editorSrc">
</div>
</div>
</div>
<div class="dashboard-editor-footer">
<button ng-show="editor.index === 1" type="button" class="btn btn-success" ng-click="add()">Add</button>
<button ng-show="editor.index === 2" type="button" class="btn btn-success pull-left" ng-click="update();">Update</button>
<button type="button" class="btn btn-success pull-right" ng-click="close_edit();dismiss();dashboard.refresh();">Close</button>
</div>
</div>

View File

@ -1,127 +1,113 @@
<div ng-controller="DashboardCtrl" body-class ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
<div ng-controller="DashboardCtrl" body-class class="dashboard" ng-class="{'dashboard-fullscreen': dashboardViewState.fullscreen}">
<div ng-include="'app/partials/pro/dashboard_topnav.html'">
</div>
<div class="submenu-controls">
<div class="submenu-panel" ng-controller="SubmenuCtrl" ng-repeat="pulldown in dashboard.pulldowns | filter:{ enable: true }">
<div class="submenu-panel-title">
<span class="small"><strong>{{pulldown.type}}:</strong></span>
</div>
<div class="submenu-panel-wrapper">
<grafana-simple-panel type="pulldown.type" ng-cloak></grafana-simple-panel>
</div>
</div>
<div class="clearfix"></div>
<div ng-if="submenuEnabled" ng-include="'app/partials/submenu.html'">
</div>
<div class="clearfix"></div>
<div class="container-fluid main">
<div>
<div class="grafana-container container">
<!-- Rows -->
<div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
<div class="row-control">
<div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<div class="row-close-buttons">
<span class="row-button bgWarning" config-modal="app/partials/roweditor.html" class="pointer">
<i bs-tooltip="'Configure row'" data-placement="right" ng-show="row.editable" class="icon-cog pointer"></i>
</span>
<span class="row-button bgPrimary" ng-click="toggle_row(row)">
<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
</span>
</div>
<span class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></span>
</div>
<div class="row-open" ng-show="!row.collapse">
<div class='row-tab bgPrimary' ng-click="toggle_row(row)">
<span class="row-tab-button">
<i class="icon-caret-right"></i>
</span>
</div>
<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
<i class="icon-th-list"></i>
</span>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
<li class="dropdown-submenu">
<a href="javascript:void(0);">Add Panel</a>
<ul class="dropdown-menu">
<li bindonce ng-repeat="name in panelNames">
<a ng-click="add_panel_default(name)" bo-text="name"></a>
</li>
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Set height</a>
<ul class="dropdown-menu">
<li><a ng-click="set_height('100px')">100 px</a></li>
<li><a ng-click="set_height('150px')">150 px</a></li>
<li><a ng-click="set_height('200px')">200 px</a></li>
<li><a ng-click="set_height('250px')">250 px</a></li>
<li><a ng-click="set_height('300px')">300 px</a></li>
<li><a ng-click="set_height('350px')">350 px</a></li>
<li><a ng-click="set_height('450px')">450 px</a></li>
<li><a ng-click="set_height('500px')">500 px</a></li>
<li><a ng-click="set_height('600px')">600 px</a></li>
<li><a ng-click="set_height('700px')">700 px</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Move</a>
<ul class="dropdown-menu">
<li><a ng-click="move_row(-1)">Up</a></li>
<li><a ng-click="move_row(1)">Down</a></li>
</ul>
</li>
<li>
<a config-modal="app/partials/roweditor.html">Row editor</a>
</li>
<li>
<a ng-click="delete_row()">Delete row</a>
</li>
</ul>
</div>
</div>
</div>
<div style="padding-top:0px" ng-if="!row.collapse">
<div class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
</div>
<div dash-editor-view>
</div>
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels"
class="panel nospace"
style="position:relative"
data-drop="true"
panel-width
ng-model="panel"
data-jqyoui-options
jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
ng-class="{'dragInProgress':dashboard.$$panelDragging}">
<grafana-panel type="panel.type" ng-cloak></grafana-panel>
</div>
<div class="main-view-container">
<div class="grafana-row" ng-controller="RowCtrl" ng-repeat="(row_name, row) in dashboard.rows" row-height>
<div class="row-control">
<div class="row-control-inner" style="padding:0px;margin:0px;position:relative;">
<div class="row-close" ng-show="row.collapse" data-placement="bottom" >
<div class="row-close-buttons">
<span class="row-button bgPrimary" ng-click="toggle_row(row)">
<i bs-tooltip="'Expand row'" data-placement="right" class="icon-caret-left pointer" ></i>
</span>
</div>
<span class="row-text pointer" ng-click="toggle_row(row)" ng-bind="row.title"></span>
</div>
<div class="row-open" ng-show="!row.collapse">
<div class='row-tab bgSuccess dropdown' ng-show="row.editable">
<span class="row-tab-button dropdown-toggle" data-toggle="dropdown">
<i class="icon-th-list"></i>
</span>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="drop1">
<li>
<a ng-click="toggle_row(row)">Collapse row</a>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Add Panel</a>
<ul class="dropdown-menu">
<li bindonce ng-repeat="name in panelNames">
<a ng-click="add_panel_default(name)" bo-text="name"></a>
</li>
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Set height</a>
<ul class="dropdown-menu">
<li><a ng-click="set_height('25px')">25 px</a></li>
<li><a ng-click="set_height('100px')">100 px</a></li>
<li><a ng-click="set_height('150px')">150 px</a></li>
<li><a ng-click="set_height('200px')">200 px</a></li>
<li><a ng-click="set_height('250px')">250 px</a></li>
<li><a ng-click="set_height('300px')">300 px</a></li>
<li><a ng-click="set_height('350px')">350 px</a></li>
<li><a ng-click="set_height('450px')">450 px</a></li>
<li><a ng-click="set_height('500px')">500 px</a></li>
<li><a ng-click="set_height('600px')">600 px</a></li>
<li><a ng-click="set_height('700px')">700 px</a></li>
</ul>
</li>
<li class="dropdown-submenu">
<a href="javascript:void(0);">Move</a>
<ul class="dropdown-menu">
<li><a ng-click="move_row(-1)">Up</a></li>
<li><a ng-click="move_row(1)">Down</a></li>
</ul>
</li>
<li>
<a dash-editor-link="app/partials/roweditor.html">Row editor</a>
</li>
<li>
<a ng-click="delete_row()">Delete row</a>
</li>
</ul>
</div>
</div>
</div>
<div panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<div style="padding-top:0px" ng-if="!row.collapse">
<div class="row-text pointer" ng-click="toggle_row(row)" ng-if="row.showTitle" ng-bind="row.title">
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
</div>
<!-- Panels -->
<div ng-repeat="(name, panel) in row.panels"
class="panel nospace"
style="position:relative"
data-drop="true"
panel-width
ng-model="panel"
data-jqyoui-options
jqyoui-droppable="{index:$index,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver(true)',onOut:'panelMoveOut'}"
ng-class="{'dragInProgress':dashboard.$$panelDragging}">
<grafana-panel type="panel.type" ng-cloak></grafana-panel>
</div>
<div panel-drop-zone class="panel dragInProgress" style="margin:5px;width:30%;background:rgba(100,100,100,0.50)" ng-style="{height:row.height}" data-drop="true" ng-model="row.panels" data-jqyoui-options jqyoui-droppable="{index:row.panels.length,mutate:false,onDrop:'panelMoveDrop',onOver:'panelMoveOver',onOut:'panelMoveOut'}">
</div>
<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
<div class="span12" style="text-align:right;">
<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
<span><i class="icon-plus-sign"></i> ADD A ROW</span>
</span>
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
</div>
</div>
</div>
<div ng-show='dashboard.editable' class="row-fluid add-row-panel-hint">
<div class="span12" style="text-align:right;">
<span style="margin-right: 10px;" ng-click="add_row_default()" class="pointer btn btn-info btn-mini">
<span><i class="icon-plus-sign"></i> ADD A ROW</span>
</span>
</div>
</div>
</div>
<div ng-include="'app/partials/console.html'" ng-if="consoleEnabled">
</div>

View File

@ -1,7 +1,10 @@
<div class="navbar navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<span class="brand"><img src="img/small.png" bs-tooltip="'Grafana'" data-placement="bottom"> {{dashboard.title}}</span>
<span class="brand">
<img class="logo-icon" src="img/fav32.png" bs-tooltip="'Grafana'" data-placement="bottom"></img>
<span class="page-title">{{dashboard.title}}</span>
</span>
<ul class="nav pull-right" ng-controller='DashboardNavCtrl' ng-init="init()">
<li ng-show="dashboardViewState.fullscreen">
@ -46,6 +49,9 @@
<li ng-show="isFavorite">
<a class="link" ng-click="removeAsFavorite()">Remove as favorite</a>
</li>
<li>
<a class="link" ng-click="editJson()">Dashboard JSON</a>
</li>
<li>
<a class="link" ng-click="exportDashboard()">Export dashboard</a>
</li>
@ -57,12 +63,15 @@
</ul>
</li>
<li class="dropdown grafana-menu-load" ng-controller="SearchCtrl" ng-init="init()" ng-include="'app/partials/search.html'">
<li class="dropdown grafana-menu-load">
<a bs-tooltip="'Search'" ng-click="openSearch()">
<i class='icon-folder-open'></i>
</a>
</li>
<li class="grafana-menu-home"><a bs-tooltip="'Goto saved default'" data-placement="bottom" href='#/'><i class='icon-home'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" config-modal="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-edit" ng-show="dashboard.editable" bs-tooltip="'Configure dashboard'" data-placement="bottom"><a class="link" dash-editor-link="app/partials/dasheditor.html"><i class='icon-cog pointer'></i></a></li>
<li class="grafana-menu-stop-playlist hide">
<a class='small' ng-click='stopPlaylist(2)'>

View File

@ -1,108 +1,113 @@
<div class="modal-body">
<div class="pull-right editor-title">Dashboard settings</div>
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-cogs"></i>
Dashboard settings
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General', 'Rows', 'Controls', 'Import']" data-title="{{tab}}">
</div>
<div ng-repeat="tab in dashboard.nav" data-title="{{tab.type}}">
</div>
</div>
<div ng-if="editor.index == 0">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
</div>
<div class="editor-option">
<label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
</div>
<div class="editor-option">
<label class="small">Time correction</label>
<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small">Hide controls (CTRL+H)</label>
<input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 1">
<div class="editor-row">
<div class="span8">
<h4>Rows</h4>
<table class="table table-striped">
<thead>
<th width="1%"></th>
<th width="1%"></th>
<th width="1%"></th>
<th width="97%">Title</th>
</thead>
<tr ng-repeat="row in dashboard.rows">
<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td><i ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="pointer icon-remove"></i></td>
<td>{{row.title}}</td>
</tr>
</table>
</div>
<div class="span4">
<h4>Add Row</h4>
<label class="small">Title</label>
<input type="text" class="input-normal" ng-model='row.title' placeholder="New row"></input>
<label class="small">Height</label>
<input type="text" class="input-mini" ng-model='row.height'></input>
</div>
</div>
</div>
<div ng-if="editor.index == 2">
<div class="editor-row">
<div class="section">
<h5>Feature toggles</h5>
<div class="editor-option" ng-repeat="pulldown in dashboard.pulldowns">
<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
</div>
<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
</div>
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General', 'Rows', 'Features', 'Import']" data-title="{{tab}}">
</div>
<div ng-repeat="tab in dashboard.nav" data-title="{{tab.type}}">
</div>
</div>
<div ng-if="editor.index == 3">
<ng-include src="'app/partials/import.html'"></ng-include>
</div>
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>
</div>
<div class="modal-footer">
<div class="pull-left grafana-version-footer" ng-if="editor.index == 0">
<span class="editor-option small">
Grafana version: {{grafanaVersion}}
</span>
<span> | <a ng-click="toggleConsole()" ng-show="!consoleEnabled">enable console</a> <a ng-click="toggleConsole()" ng-show="consoleEnabled">disable console</a></span>
<div class="small" grafana-version-check>
</div>
</div>
<div class="dashboard-editor-body">
<button ng-click="add_row(dashboard,row); reset_row();" class="btn btn-success" ng-show="editor.index == 1">Create Row</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();dashboard.emit_refresh()">Close</button>
<div ng-if="editor.index == 0">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-large" ng-model='dashboard.title'></input>
</div>
<div class="editor-option">
<label class="small">Theme</label><select class="input-small" ng-model="dashboard.style" ng-options="f for f in ['dark','light']" ng-change="styleUpdated()"></select>
</div>
<div class="editor-option">
<label class="small">Time correction</label>
<select ng-model="dashboard.timezone" class='input-small' ng-options="f for f in ['browser','utc']"></select>
</div>
<div class="editor-option">
<label class="small">Hide controls (CTRL+H)</label>
<input type="checkbox" ng-model="dashboard.hideControls" ng-checked="dashboard.hideControls">
</div>
</div>
</div>
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Tags</label>
<bootstrap-tagsinput ng-model="dashboard.tags" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
<tip>Press enter to a add tag</tip>
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 1">
<div class="editor-row">
<div class="span6">
<table class="grafana-options-table">
<tr ng-repeat="row in dashboard.rows">
<td style="width: 97%">
{{row.title}}
</td>
<td><i ng-click="_.move(dashboard.rows,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(dashboard.rows,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td>
<a ng-click="dashboard.rows = _.without(dashboard.rows,row)" class="btn btn-danger btn-mini">
<i class="icon-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
</div>
<div ng-if="editor.index == 2">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<label class="small">Templating</label>
<input type="checkbox" ng-model="dashboard.templating.enable" ng-checked="dashboard.templating.enable" ng-change="checkFeatureToggles()"x >
</div>
<div class="editor-option">
<label class="small">Annotations</label>
<input type="checkbox" ng-model="dashboard.annotations.enable" ng-checked="dashboard.annotations.enable" ng-change="checkFeatureToggles()">
</div>
<div class="editor-option" ng-repeat="pulldown in dashboard.nav">
<label class="small" style="text-transform:capitalize;">{{pulldown.type}}</label><input type="checkbox" ng-model="pulldown.enable" ng-checked="pulldown.enable">
</div>
</div>
</div>
</div>
<div ng-if="editor.index == 3">
<ng-include src="'app/partials/import.html'"></ng-include>
</div>
<div ng-repeat="pulldown in dashboard.nav" ng-controller="SubmenuCtrl" ng-show="editor.index == 4+$index">
<ng-include ng-show="pulldown.enable" src="edit_path(pulldown.type)"></ng-include>
<button ng-hide="pulldown.enable" class="btn" ng-click="pulldown.enable = true">Enable the {{pulldown.type}}</button>
</div>
</div>
<div class="clearfix"></div>
</div>
<div class="dashboard-editor-footer">
<div class="grafana-version-info" ng-show="editor.index === 0">
<span class="editor-option small">
Grafana version: {{grafanaVersion}} &nbsp;&nbsp;
</span>
<span grafana-version-check>
</span>
</div>
<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss();reset_panel();dashboard.emit_refresh()">Close</button>
</div>

View File

@ -0,0 +1,19 @@
<div ng-controller="JsonEditorCtrl">
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-edit"></i>
JSON
</div>
</div>
<div class="dashboard-editor-body" style="height: 500px">
<textarea ng-model="json" rows="20" spellcheck="false" style="width: 90%;"></textarea>
</div>
<div class="dashboard-editor-footer">
<button type="button" class="btn btn-success pull-left" ng-show="canUpdate" ng-click="update(); dismiss();">Update</button>
<button type="button" class="btn btn-success pull-right" ng-click="dismiss();">Close</button>
</div>
</div>

View File

@ -1,5 +1,4 @@
<div class="editor-row" style="margin-top: 10px;">
<div class="editor-row">
<div ng-repeat="target in panel.targets"
class="grafana-target"
@ -11,19 +10,19 @@
<ul class="grafana-target-controls">
<li ng-show="parserError">
<a bs-tooltip="parserError" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="icon-warning-sign"></i>
<i class="icon icon-warning-sign"></i>
</a>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="showTextEditor = !showTextEditor">
<i class="icon-pencil"></i>
<i class="icon icon-pencil"></i>
</a>
</li>
<li class="dropdown">
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
<i class="icon icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
@ -31,23 +30,19 @@
ng-click="duplicate()">
Duplicate
</a>
</li>
<li role="menuitem">
<a tabindex="1"
ng-click="toggleMetricOptions()">
Toggle request options
</a>
</li>
</ul>
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="icon-remove"></i>
<i class="icon icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-target-controls-left">
<ul class="grafana-segment-list">
<li class="grafana-target-segment" style="min-width: 15px; text-align: center">
{{targetLetters[$index]}}
</li>
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
@ -66,21 +61,8 @@
ng-show="showTextEditor" />
<ul class="grafana-segment-list" role="menu" ng-hide="showTextEditor">
<li class="dropdown" ng-repeat="segment in segments" role="menuitem">
<a tabindex="1"
class="grafana-target-segment dropdown-toggle"
data-toggle="dropdown"
ng-click="getAltSegments($index)"
focus-me="segment.focus"
ng-bind-html="segment.html">
</a>
<ul class="dropdown-menu scrollable grafana-segment-dropdown-menu" role="menu">
<li ng-repeat="altSegment in altSegments" role="menuitem">
<a href="javascript:void(0)" tabindex="1" ng-click="setSegment($index, $parent.$index)" ng-bind-html="altSegment.html"></a>
</li>
</ul>
</li>
<li ng-repeat="func in functions">
<li ng-repeat="segment in segments" role="menuitem" graphite-segment></li>
<li ng-repeat="func in functions">
<span graphite-func-editor class="grafana-target-segment grafana-target-function">
</span>
</li>
@ -91,24 +73,109 @@
</div>
</div>
<div class="grafana-target grafana-metric-options" ng-if="panel.metricOptionsEnabled">
</div>
<section class="grafana-metric-options">
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-wrench"></i>
</li>
<li class="grafana-target-segment">
cacheTimeout <tip>Graphite parameter to overwride memcache default timeout (unit is seconds)</tip>
cacheTimeout
</li>
<li>
<input type="text"
class="input-large grafana-target-segment-input"
ng-model="panel.cacheTimeout"
spellcheck='false'
placeholder="60">
class="input-mini grafana-target-segment-input"
ng-model="panel.cacheTimeout"
bs-tooltip="'Graphite parameter to overwride memcache default timeout (unit is seconds)'"
data-placement="right"
spellcheck='false'
placeholder="60">
</li>
</ul>
<div class="clearfix"></div>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-info-sign"></i>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
shorter legend names
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(2);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
series as parameters
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(4)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
templating
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</section>
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 1">
<h5>Shorter legend names</h5>
<ul>
<li>alias() function to specify a custom series name</li>
<li>aliasByNode(2) to alias by a specific part of your metric path</li>
<li>aliasByNode(2, -1) you can add multiple segment paths, and use negative index</li>
<li>groupByNode(2, 'sum') is useful if you have 2 wildcards in your metric path and want to sumSeries and group by</li>
</ul>
</div>
<div class="grafana-info-box span8" ng-if="editorHelpIndex === 2">
<h5>Series as parameter</h5>
<ul>
<li>Some graphite functions allow you to have many series arguments</li>
<li>Use #[A-Z] to use a graphite query as parameter to a function</li>
<li>
Examples:
<ul>
<li>asPercent(#A, #B)</li>
<li>prod.srv-01.counters.count - asPercent(#A) : percentage of count in comparison with A query</li>
<li>prod.srv-01.counters.count - sumSeries(#A) : sum count and series A </li>
<li>divideSeries(#A, #B)</li>
</ul>
</li>
<li>If a query is added only to be used as a parameter, hide it from the graph with the eye icon</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
<h5>Stacking</h5>
<ul>
<li>You find the stacking option under Display Styles tab</li>
<li>When stacking is enabled make sure null point mode is set to 'null as zero'</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 4">
<h5>Templating</h5>
<ul>
<li>You can use a template variable in place of metric names</li>
<li>You can use a template variable in place of function parameters</li>
<li>You enable the templating feature in Dashboard settings / Feature toggles </li>
</ul>
</div>
</div>
</div>

View File

@ -1,4 +1,4 @@
<div ng-controller="GraphiteImportCtrl" ng-init="init()" style="height: 400px">
<div ng-controller="GraphiteImportCtrl" ng-init="init()">
<h5>Import dashboards from graphite web</h5>
<div class="editor-row">
@ -12,7 +12,7 @@
</li>
</ul>
</div>
<button ng-click="listAll()" class="btn btn-primary">List all dashboards</button>
<button ng-click="listAll()" class="btn btn-success">List all dashboards</button>
</div>
</div>

View File

@ -1,8 +1,8 @@
<div class="editor-row">
<div class="section">
<h5>InfluxDB Query <tip>Example: select text from events where [[timeFilter]]</tip></h5>
<h5>InfluxDB Query <tip>Example: select text from events where $timeFilter</tip></h5>
<div class="editor-option">
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where [[timeFilter]]"></input>
<input type="text" class="span10" ng-model='currentAnnotation.query' placeholder="select text from events where $timeFilter"></input>
</div>
</div>
</div>

View File

@ -1,5 +1,4 @@
<div class="editor-row" style="margin-top: 10px;">
<div class="editor-row">
<div ng-repeat="target in panel.targets"
class="grafana-target"
@ -14,7 +13,7 @@
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
<i class="icon icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
@ -26,16 +25,14 @@
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="icon-remove"></i>
<i class="icon icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-target-controls-left">
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"
role="menuitem">
<a class="grafana-target-segment" ng-click="target.hide = !target.hide; get_data();" role="menuitem">
<i class="icon-eye-open"></i>
</a>
</li>
@ -45,7 +42,7 @@
<ul class="grafana-segment-list" ng-show="target.rawQuery">
<li>
<input type="text"
class="grafana-target-segment-input span8"
class="grafana-target-text-input span10"
ng-model="target.query"
placeholder="select ..."
focus-me="target.rawQuery"
@ -54,26 +51,16 @@
ng-model-onblur
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
as
</li>
<li>
<input type="text"
class="input-medium grafana-target-segment-input"
ng-model="target.alias"
spellcheck='false'
placeholder="alias"
ng-blur="get_data()">
</li>
</ul>
<!-- Query editor mode -->
<ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
<li class="grafana-target-segment">
series
</li>
<li>
<input type="text"
class="input-large grafana-target-segment-input"
class="grafana-target-text-input span8"
ng-model="target.series"
spellcheck='false'
bs-typeahead="listSeries"
@ -82,107 +69,198 @@
ng-blur="seriesBlur()">
</li>
<li class="grafana-target-segment">
select
</li>
<li class="grafana-target-segment">
alias
</li>
<li class="dropdown">
<span influxdb-func-editor class="grafana-target-segment grafana-target-function">
</span>
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="target.alias"
spellcheck='false' placeholder="alias" ng-blur="get_data()">
</li>
<li>
<a class="grafana-target-segment"
ng-click="target.condition_filter = !target.condition_filter; get_data();"
bs-tooltip="'Add a where clause'"
role="menuitem">
<i class="icon-filter"></i>
</a>
</li>
<li ng-show="target.condition_filter">
<input type="text"
class="input-small grafana-target-segment-input"
ng-model="target.condition_key"
placeholder="key"
spellcheck='false'
bs-typeahead="listColumns"
data-min-length=0
ng-blur="get_data()">
<select class="input-mini grafana-target-segment-input"
ng-change="get_data()"
ng-model="target.condition_op"
ng-options="f for f in operators" ></select>
<input type="text"
class="input-small grafana-target-segment-input"
ng-model="target.condition_value"
placeholder="value"
spellcheck='false'
data-min-length=0
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
group by time
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<!-- Raw Query mode -->
<ul class="grafana-segment-list" ng-show="target.rawQuery">
<li class="grafana-target-segment">
<i class="icon-eye-open invisible"></i>
</li>
<li class="grafana-target-segment">
alias
</li>
<li>
<input type="text"
class="input-mini grafana-target-segment-input"
ng-model="target.interval"
placeholder="{{interval}}"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
spellcheck='false'
ng-model-onblur ng-change="get_data()" >
</li>
<li>
<a class="grafana-target-segment"
ng-click="target.groupby_field_add = !target.groupby_field_add; get_data();"
bs-tooltip="'Add a group by column'"
role="menuitem">
<i class="icon-plus"></i>
</a>
</li>
<li ng-show="target.groupby_field_add">
<input type="text"
class="input-small grafana-target-segment-input"
ng-model="target.groupby_field"
placeholder="column"
spellcheck="false"
bs-typeahead="listColumns"
data-min-length=0
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
as
</li>
<li>
<input type="text"
class="input-medium grafana-target-segment-input"
class="input-medium grafana-target-text-input"
ng-model="target.alias"
spellcheck='false'
placeholder="alias"
ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
spellcheck='false' placeholder="{{interval}}" data-placement="right"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
ng-model-onblur ng-change="get_data()" >
</li>
</ul>
<!-- Query editor mode -->
<ul class="grafana-segment-list" role="menu" ng-hide="target.rawQuery">
<li class="grafana-target-segment">
<i class="icon-eye-open invisible"></i>
</li>
<li class="grafana-target-segment">
select
</li>
<li class="dropdown">
<span influxdb-func-editor class="grafana-target-segment">
</span>
</li>
<li class="grafana-target-segment">
where
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="target.condition"
bs-tooltip="'Add a where clause'" data-placement="right" spellcheck='false' placeholder="column ~= value" ng-blur="get_data()">
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text" class="input-mini grafana-target-text-input" ng-model="target.interval"
spellcheck='false' placeholder="{{interval}}" data-placement="right"
bs-tooltip="'Leave blank for auto handling based on time range and panel width'"
ng-model-onblur ng-change="get_data()" >
</li>
<li class="grafana-target-segment">
<i class="icon-plus"></i>
</li>
<li>
<input type="text" class="input-small grafana-target-text-input" ng-model="target.groupby_field" bs-tooltip="'Add a group by column or leave blank'"
placeholder="column" spellcheck="false" bs-typeahead="listColumns" data-min-length=0 ng-blur="get_data()">
</li>
<li class="dropdown">
<a class="grafana-target-segment pointer" data-toggle="dropdown" bs-tooltip="'Insert missing values, important when stacking'" data-placement="right">
<span ng-show="target.fill">
fill ({{target.fill}})
</span>
<span ng-show="!target.fill">
no fill
</span>
</a>
<ul class="dropdown-menu">
<li><a ng-click="target.fill = ''">no fill</a></li>
<li><a ng-click="target.fill = 'null'">fill (null)</a></li>
<li><a ng-click="target.fill = '0'">fill (0)</a></li>
</ul>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>
<div class="pull-left metrics-editor-help" style="margin-top: 30px;">
<div class="span6">
<span class="pointer">
<i class="icon-question-sign"></i> alias patterns:
</span>
<ul class="hide">
<li>$s = series name</li>
<li>$g = group by</li>
<li>$[0-9] part of series name for series names seperated by dots.</li>
<ul>
</div>
</div>
</div>
<section class="grafana-metric-options">
<div class="grafana-target">
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-wrench"></i>
</li>
<li class="grafana-target-segment">
group by time
</li>
<li>
<input type="text" class="input-medium grafana-target-text-input" ng-model="panel.interval" ng-blur="get_data();"
spellcheck='false' placeholder="example: >10s">
</li>
<li class="grafana-target-segment">
<i class="icon-question-sign" bs-tooltip="'Set a low limit by having a greater sign: example: >60s'" data-placement="right"></i>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="grafana-target-inner">
<ul class="grafana-segment-list">
<li class="grafana-target-segment grafana-target-segment-icon">
<i class="icon-info-sign"></i>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(1);" bs-tooltip="'click to show helpful info'" data-placement="bottom">
alias patterns
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(2)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
stacking &amp; and fill
</a>
</li>
<li class="grafana-target-segment">
<a ng-click="toggleEditorHelp(3)" bs-tooltip="'click to show helpful info'" data-placement="bottom">
group by time
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</section>
<div class="editor-row">
<div class="pull-left" style="margin-top: 30px;">
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 1">
<h5>Alias patterns</h5>
<ul>
<li>$s = series name</li>
<li>$g = group by</li>
<li>$[0-9] part of series name for series names seperated by dots.</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 2">
<h5>Stacking and fill</h5>
<ul>
<li>When stacking is enabled it important that points align</li>
<li>If there are missing points for one series it can cause gaps or missing bars</li>
<li>You must use fill(0), and select a group by time low limit</li>
<li>Use the group by time option below your queries and specify for example &gt;10s if your metrics are written every 10 seconds</li>
<li>This will insert zeros for series that are missing measurements and will make stacking work properly</li>
</ul>
</div>
<div class="grafana-info-box span6" ng-if="editorHelpIndex === 3">
<h5>Group by time</h5>
<ul>
<li>Group by time is important, otherwise the query could return many thousands of datapoints that will slow down Grafana</li>
<li>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</li>
<li>If you use fill(0) or fill(null) set a low limit for the auto group by time interval</li>
<li>The low limit can only be set in the group by time option below your queries</li>
<li>You set a low limit by adding a greater sign before the interval</li>
<li>Example: &gt;60s if you write metrics to InfluxDB every 60 seconds</li>
</ul>
</div>
</div>
</div>

View File

@ -1,69 +1,80 @@
<div class="modal-body" ng-controller="InspectCtrl" ng-init="init()">
<div class="pull-right editor-title">Inspector</div>
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-eye-open"></i>
Inspector
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in ['Request', 'Response', 'JS Error']" data-title="{{tab}}">
</div>
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in ['Request', 'Response', 'JS Error']" data-title="{{tab}}">
</div>
</div>
<div ng-if="editor.index == 0">
<h5>Request details</h5>
<table class="table table-striped small inspector-request-table">
<tr>
<td>Url</td>
<td>{{inspector.error.config.url}}</td>
</tr>
<tr>
<td>Method</td>
<td>{{inspector.error.config.method}}</td>
</tr>
<tr ng-repeat="(key, value) in inspector.error.config.headers">
<td>
{{key}}
</td>
<td>
{{value}}
</td>
</tr>
</table>
</div>
<h5>Request parameters</h5>
<table class="table table-striped small inspector-request-table">
<tr ng-repeat="param in request_parameters">
<td>
{{param.key}}
</td>
<td>
{{param.value}}
</td>
</tr>
</table>
</div>
<div class="dashboard-editor-body">
<div ng-if="editor.index == 1">
<h5 ng-if="response" ng-bind="response"></h5>
<div ng-if="editor.index == 0">
<h5>Request details</h5>
<table class="table table-striped small inspector-request-table">
<tr>
<td>Url</td>
<td>{{inspector.error.config.url}}</td>
</tr>
<tr>
<td>Method</td>
<td>{{inspector.error.config.method}}</td>
</tr>
<tr ng-repeat="(key, value) in inspector.error.config.headers">
<td>
{{key}}
</td>
<td>
{{value}}
</td>
</tr>
</table>
<div ng-if="response_html">
<div iframe-content="response_html"></div>
</div>
<h5>Request parameters</h5>
<table class="table table-striped small inspector-request-table">
<tr ng-repeat="param in request_parameters">
<td>
{{param.key}}
</td>
<td>
{{param.value}}
</td>
</tr>
</table>
</div>
</div>
<div ng-if="editor.index == 1">
<h5 ng-if="response" ng-bind="response"></h5>
<div ng-if="editor.index == 2">
<div ng-if="response_html">
<div iframe-content="response_html"></div>
</div>
<label>Message:</label>
<pre>
{{message}}
</pre>
</div>
<label>Stack trace:</label>
<pre>
{{stack_trace}}
</pre>
<div ng-if="editor.index == 2">
</div>
<label>Message:</label>
<pre>
{{message}}
</pre>
<label>Stack trace:</label>
<pre>
{{stack_trace}}
</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
<button type="button" class="btn btn-info" ng-click="dismiss()">Close</button>
</div>

View File

@ -12,7 +12,7 @@
<a class="pointer dropdown-toggle"
data-toggle="dropdown"
tabindex="1">
<i class="icon-cog"></i>
<i class="icon icon-cog"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
@ -25,12 +25,12 @@
</li>
<li>
<a class="pointer" tabindex="1" ng-click="removeDataQuery(target)">
<i class="icon-remove"></i>
<i class="icon icon-remove"></i>
</a>
</li>
</ul>
<ul class="grafana-target-controls-left">
<ul class="grafana-segment-list">
<li>
<a class="grafana-target-segment"
ng-click="target.hide = !target.hide; get_data();"

View File

@ -1,6 +0,0 @@
<div ng-include="'app/partials/panelgeneral.html'"></div>
<div ng-if="!panelMeta.fullEditorTabs" ng-include="edit_path(panel.type)"></div>
<div ng-repeat="tab in panelMeta.editorTabs">
<h5>{{tab.title}}</h5>
<div ng-include="tab.src"></div>
</div>

View File

@ -1,23 +1,30 @@
<div bindonce class="modal-body">
<div class="pull-right editor-title" bo-text="panel.type+' settings'"></div>
<div ng-model="editor.index" bs-tabs>
<div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
</div>
</div>
<div ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></div>
</div>
<div bindonce class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-text-width"></i>
<span bo-text="panel.type+' settings'"></span>
</div>
<div ng-show="editorTabs[editor.index] == 'Panel'">
<div ng-include src="edit_path(panel.type)"></div>
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in setEditorTabs(panelMeta)" data-title="{{tab}}">
</div>
</div>
<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
<div class="modal-footer">
<!-- close_edit() is provided here to allow for a scope to perform action on dismiss -->
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss()">Close</button>
</div>
<div class="dashboard-editor-body">
<div ng-show="editorTabs[editor.index] == 'General'">
<div ng-include src="'app/partials/panelgeneral.html'"></div>
</div>
<div ng-show="editorTabs[editor.index] == 'Panel'">
<div ng-include src="edit_path(panel.type)"></div>
</div>
<div ng-repeat="tab in panelMeta.editorTabs" ng-show="editorTabs[editor.index] == tab.title">
<div ng-include src="tab.src"></div>
</div>
</div>
<div class="dashboard-editor-footer">
<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss()">Close</button>
</div>

View File

@ -4,8 +4,11 @@
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-medium" ng-model='panel.title'></input>
</div>
<div class="editor-option" ng-hide="panel.sizeable == false">
<div class="editor-option">
<label class="small">Span</label> <select class="input-mini" ng-model="panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
</div>
<div class="editor-option">
<label class="small">Height</label><input type="text" class="input-small" ng-model='panel.height'></select>
</div>
</div>
</div>
</div>

View File

@ -1,55 +1,61 @@
<div ng-controller="PlaylistCtrl" ng-init="init()">
<div class="modal-header">
<h3>Start dashboard playlist</h3>
</div>
<div class="modal-body">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<table class="table table-striped span4">
<tr>
<th>Dashboard</th>
<th>Include</th>
<th style="white-space: nowrap;">Remove as favorite</th>
</tr>
<tr ng-repeat="dashboard in favDashboards">
<td style="white-space: nowrap;">
{{dashboard.title}}
</td>
<td style="text-align: center">
<input type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
</td>
<td style="text-align: center">
<i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>
</td>
</tr>
<tr ng-hide="favDashboards.length">
<td colspan="3">
<i class="icon-warning"></i> No dashboards marked as favorites
</td>
</tr>
</table>
</div>
<div class="editor-option">
<div class="span4">
<span><i class="icon-question-sign"></i>
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
<br/><br/>
</span>
</div>
</div>
<div class="editor-option">
<label>
Timespan between change
</label>
<input type="text" class="input-small" ng-model="timespan" />
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-success" ng-click="start();dismiss();"><i class="icon-play"></i> Start</button>
<button type="button" class="btn btn-primary" ng-click="dismiss();"><i class="icon-ban-circle"></i> Cancel</button>
</div>
</div>
</div>
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-play"></i>
Start dashboard playlist
</div>
</div>
<div class="dashboard-editor-body">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<table class="table table-striped span4">
<tr>
<th>Dashboard</th>
<th>Include</th>
<th style="white-space: nowrap;">Remove as favorite</th>
</tr>
<tr ng-repeat="dashboard in favDashboards">
<td style="white-space: nowrap;">
{{dashboard.title}}
</td>
<td style="text-align: center">
<input type="checkbox" ng-model="dashboard.include" ng-checked="dashboard.include" />
</td>
<td style="text-align: center">
<i class="icon-remove pointer" ng-click="removeAsFavorite(dashboard)"></i>
</td>
</tr>
<tr ng-hide="favDashboards.length">
<td colspan="3">
<i class="icon-warning"></i> No dashboards marked as favorites
</td>
</tr>
</table>
</div>
<div class="editor-option">
<div class="span4">
<span><i class="icon-question-sign"></i>
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
<br/><br/>
</span>
</div>
</div>
<div class="editor-option">
<label>
Timespan between change
</label>
<input type="text" class="input-small" ng-model="timespan" />
</div>
</div>
</div>
</div>
<div class="dashboard-editor-footer">
<button class="btn btn-success" ng-click="start();dismiss();"><i class="icon-play"></i> Start</button>
<button type="button" class="btn btn-success pull-right" ng-click="dismiss();"><i class="icon-ban-circle"></i> Close</button>
</div>
</div>

View File

@ -1,12 +1,19 @@
<div class="modal-body">
<div class="pull-right editor-title">Row settings</div>
<div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-th-list"></i>
Row settings
</div>
<div ng-model="editor.index" bs-tabs>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['General','Panels']" data-title="{{tab}}">
</div>
</div>
</div>
</div>
<div class="editor-row" ng-if="editor.index == 0">
</div>
<div class="dashboard-editor-body">
<div class="editor-row" ng-if="editor.index == 0">
<div class="editor-option">
<label class="small">Title</label><input type="text" class="input-medium" ng-model='row.title'></input>
</div>
@ -22,30 +29,32 @@
</div>
<div class="row-fluid" ng-if="editor.index == 1">
<div class="span12">
<h4>Panels</h4>
<table class="table table-condensed table-striped">
<table class="grafana-options-table" style="max-width: 400px; width: auto">
<thead>
<th>Title</th>
<th>Type</th>
<th>Span <span class="small">({{dashboard.rowSpan(row)}}/12)</span></th>
<th>Delete</th>
<th>Move</th>
<th>Span</span></th>
<th></th>
<th></th>
<th></th>
</thead>
<tr ng-repeat="panel in row.panels">
<td>{{panel.title}}</td>
<td style="width: 95%">{{panel.title}}</td>
<td>{{panel.type}}</td>
<td><select ng-hide="panel.sizeable == false" class="input-mini" ng-model="panel.span" ng-options="size for size in [1,2,3,4,5,6,7,8,9,10,11,12]"></select></td>
<td><i ng-click="row.panels = _.without(row.panels,panel)" class="pointer icon-remove"></i></td>
<td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
</tr>
</table>
</div>
</div>
<td><select ng-hide="panel.sizeable == false" class="input-mini" style="margin-bottom: 0;" ng-model="panel.span" ng-options="size for size in [1,2,3,4,5,6,7,8,9,10,11,12]"></select></td>
<td><i ng-click="_.move(row.panels,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td><i ng-click="_.move(row.panels,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td>
<a ng-click="row.panels = _.without(row.panels,panel)" class="btn btn-danger btn-mini">
<i class="icon-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
</div>
<div class="modal-footer">
<button ng-show="editor.index == 1" ng-click="editor.index = 2;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
<button ng-show="panel.type && editor.index == 2" ng-click="add_panel(panel); reset_panel(); editor.index = 1;" class="btn btn-success" ng-disabled="panel.loadingEditor">Add Panel</button>
<button type="button" class="btn btn-info" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
<div class="dashboard-editor-footer">
<button type="button" class="btn btn-success pull-right" ng-click="editor.index=0;dismiss();reset_panel();close_edit()">Close</button>
</div>

View File

@ -1,105 +1,88 @@
<style>
#grafana-search {
position: fixed;
right: 0;
left: 0;
top: 39px;
margin-right: auto;
margin-left: auto;
/* give it dimensions */
min-height: 10em;
width: 90%;
}
</style>
<div ng-controller="SearchCtrl" ng-init="init()">
<a href="#" bs-tooltip="'Search'" data-placement="bottom" ng-click="openSearch()" class="dropdown-toggle" data-toggle="dropdown">
<i class='icon-folder-open'></i>
</a>
<div class="dashboard-editor-header">
<div class="dashboard-editor-title" style="border: 0; line-height: 41px;">
<i class="icon icon-search"></i>
Search
</div>
<ul class="dropdown-menu" id="grafana-search" ng-if="searchOpened">
<li ng-if="!showImport">
<div class="grafana-search-panel">
<div class="search-field-wrapper">
<button class="btn btn-success pull-right" config-modal="app/partials/playlist.html">
<i class="icon-play"></i>
Playlist
</button>
<button class="btn btn-success pull-right" ng-click="toggleImport($event)">
<i class="icon-download-alt"></i>
Import
</button>
<button class="btn btn-success pull-right" ng-click="newDashboard()">
<i class="icon-th-large"></i>
New
</button>
<span class="position: relative;">
<input type="text"
placeholder="search dashboards, metrics, or graphs"
xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)"
ng-model="query.query" spellcheck='false'
ng-change="search()" />
<a class="search-tagview-switch" href="javascript:void(0);"
ng-class="{'active': tagsOnly}"
ng-click="showTags($event)">tags</a>
</span>
</div>
<div class="grafana-search-panel">
<div class="search-field-wrapper">
<button class="btn btn-success pull-right" dash-editor-link="app/partials/playlist.html" editor-scope="isolated">
<i class="icon-play"></i>
Playlist
</button>
<button class="btn btn-success pull-right" ng-click="toggleImport($event)">
<i class="icon-download-alt"></i>
Import
</button>
<button class="btn btn-success pull-right" ng-click="newDashboard()">
<i class="icon-th-large"></i>
New
</button>
<span style="position: relative;">
<input type="text" placeholder="search dashboards, metrics, or graphs" xng-focus="giveSearchFocus"
ng-keydown="keyDown($event)" ng-model="query.query" spellcheck='false' ng-change="search()" />
<a class="search-tagview-switch" href="javascript:void(0);" ng-class="{'active': tagsOnly}" ng-click="showTags($event)">tags</a>
</span>
</div>
</div>
</div>
<h6 ng-hide="results.dashboards.length">No dashboards matching your query were found.</h6>
<div ng-if="!showImport">
<h6 ng-hide="results.dashboards.length">No dashboards matching your query were found.</h6>
<div class="search-results-container" ng-if="tagsOnly">
<div class="row">
<div class="span6 offset1">
<div ng-repeat="tag in results.tags" class="pointer" style="width: 180px; float: left;"
ng-class="{'selected': $index === selectedIndex }"
ng-click="filterByTag(tag.term, $event)">
<a class="search-result-tag label label-tag" tag-color-from-name>
<i class="icon icon-tag"></i>
<span>{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
</div>
</div>
<div class="search-results-container" ng-if="tagsOnly">
<div ng-repeat="tag in results.tags"
class="search-result-item pointer"
ng-class="{'selected': $index === selectedIndex }"
ng-click="filterByTag(tag.term, $event)">
<a class="search-result-link" >
<i class="icon icon-tag"></i>
<span class="label label-tag">{{tag.term}} &nbsp;({{tag.count}})</span>
</a>
</div>
</div>
<div class="search-results-container" ng-if="!tagsOnly">
<div class="search-result-item pointer" bindonce ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
<div class="search-results-container" ng-if="!tagsOnly">
<div class="search-result-item pointer"
bindonce ng-repeat="row in results.dashboards"
ng-class="{'selected': $index === selectedIndex }" ng-click="goToDashboard(row.id)">
<div class="search-result-actions">
<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
</a>
<a ng-click="deleteDashboard(row.id, $event)">
<i class="icon-remove"></i> delete
</a>
</div>
<div class="search-result-tags">
<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" style="margin-right: 5px;" class="label label-tag">
{{tag}}
</a>
</div>
<a class="search-result-link">
<i class="icon icon-th-large"></i>
<span bo-text="row.title"></span>
</a>
</div>
<div class="search-result-actions small">
<a ng-click="shareDashboard(row.id, row.id, $event)" config-modal="app/partials/dashLoaderShare.html">
<i class="icon-share"></i> share &nbsp;&nbsp;&nbsp;
</a>
<a ng-click="deleteDashboard(row.id, $event)">
<i class="icon-remove"></i> delete
</a>
</div>
</div>
</li>
<div class="search-result-tags">
<a ng-click="filterByTag(tag, $event)" ng-repeat="tag in row.tags" tag-color-from-name class="label label-tag">
{{tag}}
</a>
</div>
<li ng-if="showImport" style="margin: 20px;">
<div class="editor-row">
<div class="section">
<div class="editor-option">
<h5>Local File <tip>Load dashboard JSON layout from file</tip></h5>
<form>
<input type="file" id="dashupload" dash-upload /><br>
</form>
</div>
</div>
</div>
</li>
</ul>
<a class="search-result-link">
<i class="icon icon-th-large"></i>
<span bo-text="row.title"></span>
</a>
</div>
</div>
</div>
<div class="editor-row" ng-if="showImport">
<div class="section">
<div class="editor-option">
<h5>Local File <tip>Load dashboard JSON layout from file</tip></h5>
<form>
<input type="file" id="dashupload" dash-upload/><br>
</form>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,49 @@
<div class="submenu-controls" ng-controller="SubmenuCtrl">
<div class="grafana-target">
<div class="grafana-target-inner" style="border-top: none">
<ul class="grafana-segment-list">
<li class="grafana-target-segment">
<div class="dropdown">
<a class="pointer" data-toggle="dropdown">
<i class="icon-cog"></i>
</a>
<ul class="dropdown-menu">
<li><a class="pointer" dash-editor-link="app/partials/templating_editor.html">Templating</a></li>
<li><a class="pointer" dash-editor-link="app/partials/annotations_editor.html">Annotations</a></li>
</ul>
</div>
</li>
</ul>
<ul class="grafana-segment-list" ng-if="dashboard.templating.enable">
<li class="small grafana-target-segment">
<strong>VARIABLES</strong>
</li>
<li ng-repeat-start="variable in variables" class="grafana-target-segment template-param-name">
<span class="template-variable ">
${{variable.name}}:
</span>
</li>
<li ng-repeat-end template-param-selector>
</li>
</ul>
<ul class="grafana-segment-list" ng-if="dashboard.annotations.enable">
<li class="small grafana-target-segment">
<strong>ANNOTATIONS</strong>
</li>
<li ng-repeat="annotation in dashboard.annotations.list" class="grafana-target-segment annotation-segment" ng-class="{'annotation-disabled': !annotation.enable}">
<a ng-click="disableAnnotation(annotation)">
<i class="annotation-color-icon icon-bolt"></i>
{{annotation.name}}
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</div>

View File

@ -0,0 +1,170 @@
<div ng-controller="TemplateEditorCtrl" ng-init="init()"> <div class="dashboard-editor-header">
<div class="dashboard-editor-title">
<i class="icon icon-code"></i>
Templating
</div>
<div ng-model="editor.index" bs-tabs style="text-transform:capitalize;">
<div ng-repeat="tab in ['Variables', 'Add', 'Edit']" data-title="{{tab}}">
</div>
</div>
</div>
<div class="dashboard-editor-body">
<div ng-if="editor.index == 0">
<div class="editor-row row">
<div class="span8">
<div ng-if="variables.length === 0">
<em>No template variables defined</em>
</div>
<table class="grafana-options-table">
<tr ng-repeat="variable in variables">
<td style="width: 1%">
<span class="template-variable">
${{variable.name}}
</span>
</td>
<td class="max-width" style="max-width: 200px;">
{{variable.query}}
</td>
<td style="width: 1%">
<a ng-click="edit(variable)" class="btn btn-success btn-mini">
<i class="icon-edit"></i>
Edit
</a>
</td>
<td style="width: 1%"><i ng-click="_.move(variables,$index,$index-1)" ng-hide="$first" class="pointer icon-arrow-up"></i></td>
<td style="width: 1%"><i ng-click="_.move(variables,$index,$index+1)" ng-hide="$last" class="pointer icon-arrow-down"></i></td>
<td style="width: 1%">
<a ng-click="removeVariable(variable)" class="btn btn-danger btn-mini">
<i class="icon-remove"></i>
</a>
</td>
</tr>
</table>
</div>
</div>
</div>
<div ng-if="editor.index == 1 || (editor.index == 2 && !currentIsNew)">
<div class="row">
<div class="editor-option">
<div class="editor-row">
<div class="editor-option">
<label class="small">Variable name</label>
<input type="text" class="input-medium" ng-model='current.name' placeholder="name"></input>
</div>
<div class="editor-option">
<label class="small">Type</label>
<select class="input-medium" ng-model="current.type" ng-options="f for f in ['query', 'interval', 'custom']" ng-change="typeChanged()"></select>
</div>
<div class="editor-option" ng-show="current.type === 'query'">
<label class="small">Datasource</label>
<select class="input input-medium" ng-model="current.datasource" ng-options="f.value as f.name for f in datasources"></select>
</div>
<div class="editor-option text-center" ng-show="current.type === 'query'">
<label class="small">Refresh on load <tip>Check if you want values to be updated on dashboard load, will slow down dashboard load time.</tip></label>
<input type="checkbox" ng-model="current.refresh" ng-checked="current.refresh">
</div>
</div>
<div ng-show="current.type === 'interval'">
<div class="editor-row">
<div class="editor-option">
<label class="small">Values</label>
<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="name"></input>
</div>
</div>
<div class="editor-row">
<div class="editor-option text-center">
<label class="small">Include auto interval</label>
<input type="checkbox" ng-model="current.auto" ng-checked="current.auto" ng-change="runQuery()">
</div>
<div class="editor-option" ng-show="current.auto">
<label class="small">Auto interval steps <tip>How many steps, roughly, the interval is rounded and will not always match this count<tip></label>
<select class="input-mini" ng-model="current.auto_count" ng-options="f for f in [3,5,10,30,50,100,200]" ng-change="runQuery()"></select>
</div>
</div>
</div>
<div ng-show="current.type === 'custom'">
<div class="editor-row">
<div class="editor-option">
<label class="small">Values seperated by comma</label>
<input type="text" class="input-xxlarge" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue"></input>
</div>
</div>
</div>
<div ng-show="current.type === 'query'">
<div class="editor-row">
<div class="editor-option form-inline">
<label class="small">Variable values query</label>
<input type="text" class="input-xxlarge" ng-model='current.query' placeholder="apps.servers.*"></input>
<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'Execute query'" data-placement="right"><i class="icon-play"></i></button>
</div>
</div>
<div class="editor-row" style="margin: 15px 0">
<div class="editor-option form-inline">
<label class="small">regex (optional, if you want to extract part of a series name or metric node segment)</label>
<input type="text" class="input-xxlarge" ng-model='current.regex' placeholder="/.*-(.*)-.*/"></input>
<button class="btn btn-small btn-success" ng-click="runQuery()" bs-tooltip="'execute query'" data-placement="right"><i class="icon-play"></i></button>
</div>
</div>
<div class="editor-row" style="margin: 15px 0">
<div class="editor-option text-center">
<label class="small">All option</label>
<input type="checkbox" ng-model="current.includeAll" ng-checked="current.includeAll" ng-change="runQuery()">
</div>
<div class="editor-option" ng-show="current.includeAll">
<label class="small">All format</label>
<select class="input-medium" ng-model="current.allFormat" ng-change="runQuery()" ng-options="f for f in ['glob', 'wildcard', 'regex wildcard', 'regex values']"></select>
</div>
<div class="editor-option" ng-show="current.includeAll">
<label class="small">All value</label>
<input type="text" class="input-xlarge" ng-model='current.options[0].value'></input>
</div>
</div>
</div>
</div>
<div class="editor-option">
<div class="editor-row">
<div class="editor-option" >
<label class="small">Variable values (showing 20/{{current.options.length}})</label>
<ul class="grafana-options-list">
<li ng-repeat="option in current.options | limitTo: 20">
{{option.text}}
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="dashboard-editor-footer">
<button type="button" class="btn btn-success pull-left" ng-show="editor.index === 2" ng-click="update();">Update</button>
<button type="button" class="btn btn-success pull-left" ng-show="editor.index === 1" ng-click="add();">Add</button>
<button type="button" class="btn btn-success pull-right" ng-click="dismiss();">Close</button>
</div>
</div>
<!-- <div class="span4"> -->
<!-- <tip class="info&#45;box"> -->
<!-- <i class="icon&#45;question&#45;sign"></i> -->
<!-- The templating feature in Grafana lets easily create and manage templated queries. Templated queries use [[replacement]] syntax -->
<!-- to replace a part of your metric key or query. By using this feature you can make your dashboards more generic. You can for example create -->
<!-- a query replacement for your cluster name or server name. Then use that replacement in your metric queries and change -->
<!-- it globably for all graphs on the dashboard. -->
<!-- <br/><br/> -->
<!-- </tip> -->
<!-- </div> -->

View File

@ -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');
});
});
});

View File

@ -1,7 +1,9 @@
define([
'./alertSrv',
'./datasourceSrv',
'./filterSrv',
'./timeSrv',
'./templateSrv',
'./templateValuesSrv',
'./panelSrv',
'./timer',
'./panelMove',

View File

@ -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);

View File

@ -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 });
};

View File

@ -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]);
}
}

View File

@ -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);
}

View File

@ -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() {

View File

@ -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<string.length; i++) {
character = string[i];
if (character === opener) {
count++;
} else if (character === closer) {
count--;
}
}
return count > 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
});
}

View File

@ -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;
});
});

View File

@ -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) {

View File

@ -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;
};

View File

@ -128,6 +128,7 @@ define([
i === 93 || // templateEnd ]
i === 63 || // ?
i === 37 || // %
i === 35 || // #
i >= 97 && i <= 122; // a-z
}

View File

@ -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;

View File

@ -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;
});

View File

@ -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);
}

View File

@ -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));

View File

@ -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;
}
};

View File

@ -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();
}
}
};
});

View File

@ -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 '<span class="template-variable">' + match + '</span>';
}
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;
});
};
});
});

View File

@ -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});
};
});
});

121
src/app/services/timeSrv.js Normal file
View File

@ -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)
};
}
};
});
});

View File

@ -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' });

View File

@ -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 - <dashboard title>'
window_title_prefix: 'Grafana - ',
// Add your own custom pannels
plugins: {
// list of plugin panels

View File

@ -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);
}

View File

@ -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 {

View File

@ -8,8 +8,6 @@
}
.grafana-console-header {
background: @fullEditTabsBackground;
border-top: @fullEditTabsBorder;
padding: 2px 5px;
}

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}

71
src/css/less/panel.less Normal file
View File

@ -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;
}

Some files were not shown because too many files have changed in this diff Show More