Merge branch 'master' into export-dashboard

Conflicts:
	public/app/features/dashboard/submenu/submenu.ts
This commit is contained in:
Torkel Ödegaard 2016-05-25 12:32:56 +02:00
commit ba6573af61
48 changed files with 423 additions and 185 deletions

View File

@ -1,20 +1,19 @@
Thank you! For helping us make Grafana even better.
Thank you for helping us make Grafana even better!
To help us respond to your issues faster, please make sure to add as much information as possible.
To help us respond to your issues more quickly, please make sure to add as much information as possible.
If this issue is about a plugin, please open the issue in that repository.
If this issue is about a plugin, please open the issue in that plugin's repository.
Start your issues title with [Feature Request] / [Bug] / [Question] or no tag if your unsure. Also, please be aware that GitHub now supports uploading of screenshots; look at the bottom of this input field.
Start your issue's title with [Feature Request] / [Bug] / [Question] or no tag if you're unsure. Also, please be aware that GitHub now supports uploading of screenshots; look at the bottom of this input field.
Please include some basic information:
- What grafana version are you using?
- What Grafana version are you using?
- What datasource are you using?
- What OS are you running grafana on?
- What did you do?
- What was the expected result?
- What happenend instead?
If you question/bug relates to a metric query / unexpected data visualization, please include:
If your question/bug relates to a metric query / unexpected data visualization, please include:
- An image or text representation of your metric query
- The raw query and response from your data source (check this in chrome dev tools network tab)

View File

@ -1,13 +1,33 @@
# 3.1.0
# 3.1.0 (unreleased)
### Enhancements
* **Dashboard Url**: Time range changes updates url, closes [#458](https://github.com/grafana/grafana/issues/458)
* **Dashboard Url**: Template variable change updates url, closes [#5002](https://github.com/grafana/grafana/issues/5002)
* **Singlestat**: Add support for range to text mappings, closes [#1319](https://github.com/grafana/grafana/issues/1319)
* **Graph**: Adds sort order options for graph tooltip, closes [#1189](https://github.com/grafana/grafana/issues/1189)
* **Theme**: Add default theme to config file [#5011](https://github.com/grafana/grafana/pull/5011)
* **Page Footer**: Added page footer with links to docs, shows Grafana version and info if new version is available, closes [#4889](https://github.com/grafana/grafana/pull/4889)
# 3.0.3 Patch release (unreleased)
# 3.0.4 Patch release (2016-05-25)
* **Panel**: Fixed blank dashboard issue when switching to other dashboard while in fullscreen edit mode, fixes [#5163](https://github.com/grafana/grafana/pull/5163)
* **Templating**: Fixed issue with nested multi select variables and cascading and updating child variable selection state, fixes [#4861](https://github.com/grafana/grafana/pull/4861)
* **Templating**: Fixed issue with using templated data source in another template variable query, fixes [#5165](https://github.com/grafana/grafana/pull/5165)
* **Singlestat gauge**: Fixed issue with gauge render position, fixes [#5143](https://github.com/grafana/grafana/pull/5143)
* **Home dashboard**: Fixes broken home dashboard api, fixes [#5167](https://github.com/grafana/grafana/issues/5167)
# 3.0.3 Patch release (2016-05-23)
* **Annotations**: Annotations can now use a template variable as data source, closes [#5054](https://github.com/grafana/grafana/issues/5054)
* **Time picker**: Fixed issue timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
* **CloudWatch**: Support for Multiple Account by AssumeRole, closes [#3522](https://github.com/grafana/grafana/issues/3522)
* **Singlestat**: Fixed alignment and minium height issue, fixes [#5113](https://github.com/grafana/grafana/issues/5113), fixes [#4679](https://github.com/grafana/grafana/issues/4679)
* **Share modal**: Fixed link when using grafana under dashboard sub url, fixes [#5109](https://github.com/grafana/grafana/issues/5109)
* **Prometheus**: Fixed bug in query editor that caused it not to load when reloading page, fixes [#5107](https://github.com/grafana/grafana/issues/5107)
* **Elasticsearch**: Fixed bug when template variable query returns numeric values, fixes [#5097](https://github.com/grafana/grafana/issues/5097), fixes [#5088](https://github.com/grafana/grafana/issues/5088)
* **Logging**: Fixed issue with reading logging level value, fixes [#5079](https://github.com/grafana/grafana/issues/5079)
* **Timepicker**: Fixed issue with timepicker and UTC when reading time from URL, fixes [#5078](https://github.com/grafana/grafana/issues/5078)
* **Docs**: Added docs for org & user preferences HTTP API, closes [#5069](https://github.com/grafana/grafana/issues/5069)
* **Plugin list panel**: Now shows correct enable state for apps when not enabled, fixes [#5068](https://github.com/grafana/grafana/issues/5068)
* **Elasticsearch**: Templating & Annotation queries that use template variables are now formatted correctly, fixes [#5135](https://github.com/grafana/grafana/issues/5135)
# 3.0.2 Patch release (2016-05-16)

View File

@ -226,7 +226,7 @@ organization to be created for that new user.
The role new users will be assigned for the main organization (if the
above setting is set to true). Defaults to `Viewer`, other valid
options are `Admin` and `Editor`.
options are `Admin` and `Editor` and `Read-Only Editor`.
<hr>

View File

@ -10,13 +10,13 @@ page_keywords: grafana, installation, debian, ubuntu, guide
Description | Download
------------ | -------------
Stable .deb for Debian-based Linux | [grafana_3.0.2-1463383025_amd64.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.2-1463383025_amd64.deb)
Stable .deb for Debian-based Linux | [grafana_3.0.4-1464167696.deb](https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb)
## Install Stable
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.2-1463383025_amd64.deb
$ wget https://grafanarel.s3.amazonaws.com/builds/grafana_3.0.4-1464167696_amd64.deb
$ sudo apt-get install -y adduser libfontconfig
$ sudo dpkg -i grafana_3.0.2-1463383025_amd64.deb
$ sudo dpkg -i grafana_3.0.4-1464167696_amd64.deb
## APT Repository

View File

@ -10,24 +10,24 @@ page_keywords: grafana, installation, centos, fedora, opensuse, redhat, guide
Description | Download
------------ | -------------
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-3.0.2-1463383025.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.2-1463383025.x86_64.rpm)
Stable .RPM for CentOS / Fedora / OpenSuse / Redhat Linux | [grafana-3.0.4-1464167696.x86_64.rpm](https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm)
## Install Stable Release from package file
You can install Grafana using Yum directly.
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.2-1463383025.x86_64.rpm
$ sudo yum install https://grafanarel.s3.amazonaws.com/builds/grafana-3.0.4-1464167696.x86_64.rpm
Or install manually using `rpm`.
#### On CentOS / Fedora / Redhat:
$ sudo yum install initscripts fontconfig
$ sudo rpm -Uvh grafana-3.0.2-1463383025.x86_64.rpm
$ sudo rpm -Uvh grafana-3.0.4-1464167696.x86_64.rpm
#### On OpenSuse:
$ sudo rpm -i --nodeps grafana-3.0.2-1463383025.x86_64.rpm
$ sudo rpm -i --nodeps grafana-3.0.4-1464167696.x86_64.rpm
## Install via YUM Repository

View File

@ -10,7 +10,7 @@ page_keywords: grafana, installation, windows guide
Description | Download
------------ | -------------
Stable Zip package for Windows | [grafana.3.0.2.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.2.windows-x64.zip)
Stable Zip package for Windows | [grafana.3.0.4.windows-x64.zip](https://grafanarel.s3.amazonaws.com/winbuilds/dist/grafana-3.0.4.windows-x64.zip)
## Configure

View File

@ -1,4 +1,4 @@
{
"stable": "3.0.2",
"testing": "3.0.2"
"stable": "3.0.4",
"testing": "3.0.4"
}

View File

@ -1,22 +1,20 @@
#! /usr/bin/env bash
deb_ver=3.0.1
rpm_ver=3.0.1-1
deb_ver=3.0.4-1464167696
rpm_ver=3.0.4-1464167696
#rpm_ver=3.0.0-1
wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
#wget https://grafanarel.s3.amazonaws.com/builds/grafana_${deb_ver}_amd64.deb
package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
#package_cloud push grafana/stable/debian/jessie grafana_${deb_ver}_amd64.deb
#package_cloud push grafana/stable/debian/wheezy grafana_${deb_ver}_amd64.deb
package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb
package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb
#package_cloud push grafana/testing/debian/jessie grafana_${deb_ver}_amd64.deb
#package_cloud push grafana/testing/debian/wheezy grafana_${deb_ver}_amd64.deb
wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm
#wget https://grafanarel.s3.amazonaws.com/builds/grafana-${rpm_ver}.x86_64.rpm
#package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm
#package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm
package_cloud push grafana/testing/el/6 grafana-${rpm_ver}.x86_64.rpm
package_cloud push grafana/testing/el/7 grafana-${rpm_ver}.x86_64.rpm
package_cloud push grafana/stable/el/7 grafana-${rpm_ver}.x86_64.rpm
package_cloud push grafana/stable/el/6 grafana-${rpm_ver}.x86_64.rpm

View File

@ -211,7 +211,7 @@ func Register(r *macaron.Macaron) {
r.Combo("/db/:slug").Get(GetDashboard).Delete(DeleteDashboard)
r.Post("/db", reqEditorRole, bind(m.SaveDashboardCommand{}), PostDashboard)
r.Get("/file/:file", GetDashboardFromJsonFile)
r.Get("/home", GetHomeDashboard)
r.Get("/home", wrap(GetHomeDashboard))
r.Get("/tags", GetDashboardTags)
r.Post("/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard))
})

View File

@ -57,11 +57,12 @@ var awsCredentialCache map[string]cache = make(map[string]cache)
var credentialCacheLock sync.RWMutex
func getCredentials(profile string, region string, assumeRoleArn string) *credentials.Credentials {
cacheKey := profile + ":" + assumeRoleArn
credentialCacheLock.RLock()
if _, ok := awsCredentialCache[profile]; ok {
if awsCredentialCache[profile].expiration != nil &&
(*awsCredentialCache[profile].expiration).After(time.Now().UTC()) {
result := awsCredentialCache[profile].credential
if _, ok := awsCredentialCache[cacheKey]; ok {
if awsCredentialCache[cacheKey].expiration != nil &&
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
result := awsCredentialCache[cacheKey].credential
credentialCacheLock.RUnlock()
return result
}
@ -118,7 +119,7 @@ func getCredentials(profile string, region string, assumeRoleArn string) *creden
&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
})
credentialCacheLock.Lock()
awsCredentialCache[profile] = cache{
awsCredentialCache[cacheKey] = cache{
credential: creds,
expiration: expiration,
}

View File

@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/metrics"
"github.com/grafana/grafana/pkg/middleware"
m "github.com/grafana/grafana/pkg/models"
@ -158,30 +159,27 @@ func canEditDashboard(role m.RoleType) bool {
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
}
func GetHomeDashboard(c *middleware.Context) {
func GetHomeDashboard(c *middleware.Context) Response {
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
if err := bus.Dispatch(&prefsQuery); err != nil {
c.JsonApiErr(500, "Failed to get preferences", err)
return ApiError(500, "Failed to get preferences", err)
}
if prefsQuery.Result.HomeDashboardId != 0 {
slugQuery := m.GetDashboardSlugByIdQuery{Id: prefsQuery.Result.HomeDashboardId}
err := bus.Dispatch(&slugQuery)
if err != nil {
c.JsonApiErr(500, "Failed to get slug from database", err)
return
if err == nil {
dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
return Json(200, &dashRedirect)
} else {
log.Warn("Failed to get slug from database, %s", err.Error())
}
dashRedirect := dtos.DashboardRedirect{RedirectUri: "db/" + slugQuery.Result}
c.JSON(200, &dashRedirect)
return
}
filePath := path.Join(setting.StaticRootPath, "dashboards/home.json")
file, err := os.Open(filePath)
if err != nil {
c.JsonApiErr(500, "Failed to load home dashboard", err)
return
return ApiError(500, "Failed to load home dashboard", err)
}
dash := dtos.DashboardFullWithMeta{}
@ -189,11 +187,10 @@ func GetHomeDashboard(c *middleware.Context) {
dash.Meta.CanEdit = canEditDashboard(c.OrgRole)
jsonParser := json.NewDecoder(file)
if err := jsonParser.Decode(&dash.Dashboard); err != nil {
c.JsonApiErr(500, "Failed to load home dashboard", err)
return
return ApiError(500, "Failed to load home dashboard", err)
}
c.JSON(200, &dash)
return Json(200, &dash)
}
func GetDashboardFromJsonFile(c *middleware.Context) {

View File

@ -1,13 +1,17 @@
package dtos
type IndexViewData struct {
User *CurrentUser
Settings map[string]interface{}
AppUrl string
AppSubUrl string
GoogleAnalyticsId string
GoogleTagManagerId string
MainNavLinks []*NavLink
User *CurrentUser
Settings map[string]interface{}
AppUrl string
AppSubUrl string
GoogleAnalyticsId string
GoogleTagManagerId string
MainNavLinks []*NavLink
BuildVersion string
BuildCommit string
NewGrafanaVersionExists bool
NewGrafanaVersion string
}
type PluginCss struct {

View File

@ -36,11 +36,15 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
LightTheme: prefs.Theme == "light",
Timezone: prefs.Timezone,
},
Settings: settings,
AppUrl: setting.AppUrl,
AppSubUrl: setting.AppSubUrl,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId,
Settings: settings,
AppUrl: setting.AppUrl,
AppSubUrl: setting.AppSubUrl,
GoogleAnalyticsId: setting.GoogleAnalyticsId,
GoogleTagManagerId: setting.GoogleTagManagerId,
BuildVersion: setting.BuildVersion,
BuildCommit: setting.BuildCommit,
NewGrafanaVersion: plugins.GrafanaLatestVersion,
NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
}
if setting.DisableGravatar {

View File

@ -88,7 +88,7 @@ func NewApiPluginProxy(ctx *middleware.Context, proxyPath string, route *plugins
}
for key, value := range headers {
log.Info("setting key %v value %v", key, value[0])
log.Trace("setting key %v value %v", key, value[0])
req.Header.Set(key, value[0])
}
}

View File

@ -24,7 +24,7 @@ import (
"github.com/grafana/grafana/pkg/social"
)
var version = "3.0.0-beta4"
var version = "3.1.0"
var commit = "NA"
var buildstamp string
var build_date string

View File

@ -35,15 +35,6 @@ function (angular, coreModule, config) {
}
};
// build info view model
$scope.buildInfo = {
version: config.buildInfo.version,
commit: config.buildInfo.commit,
buildstamp: new Date(config.buildInfo.buildstamp * 1000),
latestVersion: config.buildInfo.latestVersion,
hasUpdate: config.buildInfo.hasUpdate,
};
$scope.submit = function() {
if ($scope.loginMode) {
$scope.login();

View File

@ -209,7 +209,9 @@ function (_, $, coreModule) {
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(function() {
$scope.onChange();
$scope.$apply(function() {
$scope.onChange();
});
});
};

View File

@ -66,14 +66,17 @@ function (angular, _, coreModule, config) {
};
this.getAnnotationSources = function() {
return _.reduce(config.datasources, function(memo, value) {
var sources = [];
this.addDataSourceVariables(sources);
_.each(config.datasources, function(value) {
if (value.meta && value.meta.annotations) {
memo.push(value);
sources.push(value);
}
});
return memo;
}, []);
return sources;
};
this.getMetricSources = function(options) {
@ -90,24 +93,7 @@ function (angular, _, coreModule, config) {
});
if (!options || !options.skipVariables) {
// look for data source variables
for (var i = 0; i < templateSrv.variables.length; i++) {
var variable = templateSrv.variables[i];
if (variable.type !== 'datasource') {
continue;
}
var first = variable.current.value;
var ds = config.datasources[first];
if (ds) {
metricSources.push({
name: '$' + variable.name,
value: '$' + variable.name,
meta: ds.meta,
});
}
}
this.addDataSourceVariables(metricSources);
}
metricSources.sort(function(a, b) {
@ -123,6 +109,27 @@ function (angular, _, coreModule, config) {
return metricSources;
};
this.addDataSourceVariables = function(list) {
// look for data source variables
for (var i = 0; i < templateSrv.variables.length; i++) {
var variable = templateSrv.variables[i];
if (variable.type !== 'datasource') {
continue;
}
var first = variable.current.value;
var ds = config.datasources[first];
if (ds) {
list.push({
name: '$' + variable.name,
value: '$' + variable.name,
meta: ds.meta,
});
}
}
};
this.init();
});
});

View File

@ -173,8 +173,8 @@ export default class TimeSeries {
isMsResolutionNeeded() {
for (var i = 0; i < this.datapoints.length; i++) {
if (this.datapoints[i][0] !== null) {
var timestamp = this.datapoints[i][0].toString();
if (this.datapoints[i][1] !== null) {
var timestamp = this.datapoints[i][1].toString();
if (timestamp.length === 13 && (timestamp % 1000) !== 0) {
return true;
}

View File

@ -30,7 +30,7 @@ function (angular, _, $) {
$scope.datasourceChanged = function() {
return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
$scope.currentDatasource = ds;
$scope.currentAnnotation.datasource = ds.name;
$scope.currentAnnotation.datasource = $scope.currentAnnotation.datasource;
});
};

View File

@ -142,12 +142,19 @@ function (angular, _, config) {
});
module.directive('panelWidth', function() {
return function(scope, element) {
var fullscreen = false;
function updateWidth() {
element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
if (!fullscreen) {
element[0].style.width = ((scope.panel.span / 1.2) * 10) + '%';
}
}
scope.onAppEvent('panel-fullscreen-enter', function(evt, info) {
fullscreen = true;
if (scope.panel.id !== info.panelId) {
element.hide();
} else {
@ -156,14 +163,20 @@ function (angular, _, config) {
});
scope.onAppEvent('panel-fullscreen-exit', function(evt, info) {
fullscreen = false;
if (scope.panel.id !== info.panelId) {
element.show();
} else {
updateWidth();
}
updateWidth();
});
scope.$watch('panel.span', updateWidth);
if (fullscreen) {
element.hide();
}
};
});

View File

@ -74,12 +74,12 @@ function (angular, _, require, config) {
$scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
var soloUrl = $scope.shareUrl;
soloUrl = soloUrl.replace('/dashboard/', '/dashboard-solo/');
soloUrl = soloUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
soloUrl = soloUrl.replace("&fullscreen", "");
$scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
$scope.imageUrl = soloUrl.replace('/dashboard-solo/', '/render/dashboard-solo/');
$scope.imageUrl = soloUrl.replace(config.appSubUrl + '/dashboard-solo/', config.appSubUrl + '/render/dashboard-solo/');
$scope.imageUrl += '&width=1000';
$scope.imageUrl += '&height=500';
};

View File

@ -1,6 +1,7 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import _ from 'lodash';
export class SubmenuCtrl {
annotations: any;
@ -8,7 +9,11 @@ export class SubmenuCtrl {
dashboard: any;
/** @ngInject */
constructor(private $rootScope, private templateValuesSrv) {
constructor(private $rootScope,
private templateValuesSrv,
private templateSrv,
private dynamicDashboardSrv,
private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
}
@ -22,8 +27,26 @@ export class SubmenuCtrl {
return this.templateValuesSrv.getValuesForTag(variable, tagKey);
}
updateUrlParamsWithCurrentVariables() {
// update url
var params = this.$location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
this.templateSrv.fillVariableValuesForUrl(params);
// update url
this.$location.search(params);
}
variableUpdated(variable) {
this.templateValuesSrv.variableUpdated(variable).then(() => {
this.dynamicDashboardSrv.update(this.dashboard);
this.updateUrlParamsWithCurrentVariables();
this.$rootScope.$emit('template-variable-value-updated');
this.$rootScope.$broadcast('refresh');
});

View File

@ -10,7 +10,7 @@ define([
var module = angular.module('grafana.services');
module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer) {
module.service('timeSrv', function($rootScope, $timeout, $routeParams, timer, $location) {
var self = this;
this.init = function(dashboard) {
@ -108,6 +108,13 @@ define([
this.old_refresh = null;
}
// update url params
var urlParams = $location.search();
var urlRange = this.timeRangeForUrl();
urlParams.from = urlRange.from;
urlParams.to = urlRange.to;
$location.search(urlParams);
$rootScope.appEvent('time-range-changed', this.time);
$timeout(this.refreshDashboard, 0);
};

View File

@ -8,6 +8,7 @@ import $ from 'jquery';
const TITLE_HEIGHT = 25;
const EMPTY_TITLE_HEIGHT = 9;
const PANEL_PADDING = 5;
const PANEL_BORDER = 2;
import {Emitter} from 'app/core/core';
@ -90,6 +91,23 @@ export class PanelCtrl {
this.addEditorTab('General', 'public/app/partials/panelgeneral.html');
this.editModeInitiated = true;
this.events.emit('init-edit-mode', null);
var routeParams = this.$injector.get('$routeParams');
if (routeParams.editorTab) {
this.editorTabs.forEach((tab, i) => {
if (tab.title === routeParams.editorTab) {
this.editorTabIndex = i;
}
});
}
}
changeTab(newIndex) {
this.editorTabIndex = newIndex;
var route = this.$injector.get('$route');
route.current.params.editorTab = this.editorTabs[newIndex].title;
route.updateParams();
}
addEditorTab(title, directiveFn, index?) {
@ -141,7 +159,7 @@ export class PanelCtrl {
}
}
this.height = this.containerHeight - (PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
this.height = this.containerHeight - (PANEL_BORDER + PANEL_PADDING + (this.panel.title ? TITLE_HEIGHT : EMPTY_TITLE_HEIGHT));
}
render(payload?) {

View File

@ -36,7 +36,7 @@ var panelTemplate = `
<ul class="gf-tabs">
<li class="gf-tabs-item" ng-repeat="tab in ::ctrl.editorTabs">
<a class="gf-tabs-link" ng-click="ctrl.editorTabIndex = $index" ng-class="{active: ctrl.editorTabIndex === $index}">
<a class="gf-tabs-link" ng-click="ctrl.changeTab($index)" ng-class="{active: ctrl.editorTabIndex === $index}">
{{::tab.title}}
</a>
</li>

View File

@ -42,6 +42,16 @@ function (angular, _) {
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
}
this.luceneFormat = function(value) {
if (typeof value === 'string') {
return luceneEscape(value);
}
var quotedValues = _.map(value, function(val) {
return '\"' + luceneEscape(val) + '\"';
});
return '(' + quotedValues.join(' OR ') + ')';
};
this.formatValue = function(value, format, variable) {
// for some scopedVars there is no variable
variable = variable || {};
@ -60,13 +70,7 @@ function (angular, _) {
return '(' + escapedValues.join('|') + ')';
}
case "lucene": {
if (typeof value === 'string') {
return luceneEscape(value);
}
var quotedValues = _.map(value, function(val) {
return '\"' + luceneEscape(val) + '\"';
});
return '(' + quotedValues.join(' OR ') + ')';
return this.luceneFormat(value, format, variable);
}
case "pipe": {
if (typeof value === 'string') {
@ -97,8 +101,11 @@ function (angular, _) {
if (!str) {
return false;
}
var match = this._regex.exec(str);
return match && (match[1] === variableName || match[2] === variableName);
variableName = regexEscape(variableName);
var findVarRegex = new RegExp('\\$(' + variableName + ')(?:\\W|$)|\\[\\[(' + variableName + ')\\]\\]', 'g');
var match = findVarRegex.exec(str);
return match !== null;
};
this.highlightVariablesAsHtml = function(str) {

View File

@ -79,7 +79,6 @@ function (angular, _, kbn) {
else if (variable.refresh === 1 || variable.refresh === 2) {
return self.updateOptions(variable).then(function() {
if (_.isEmpty(variable.current) && variable.options.length) {
console.log("setting current for %s", variable.name);
self.setVariableValue(variable, variable.options[0]);
}
lock.resolve();
@ -102,7 +101,10 @@ function (angular, _, kbn) {
}
return promise.then(function() {
var option = _.findWhere(variable.options, { text: urlValue });
var option = _.find(variable.options, function(op) {
return op.text === urlValue || op.value === urlValue;
});
option = option || { text: urlValue, value: urlValue };
self.updateAutoInterval(variable);
@ -244,15 +246,26 @@ function (angular, _, kbn) {
this.validateVariableSelectionState = function(variable) {
if (!variable.current) {
if (!variable.options.length) { return; }
return self.setVariableValue(variable, variable.options[0], true);
return self.setVariableValue(variable, variable.options[0], false);
}
if (_.isArray(variable.current.value)) {
self.selectOptionsForCurrentValue(variable);
// updated selected value
var selected = {
value: _.map(_.filter(variable.options, {selected: true}), function(op) {
return op.value;
})
};
// if none pick first
if (selected.value.length === 0) {
selected = variable.options[0];
}
return self.setVariableValue(variable, selected, false);
} else {
var currentOption = _.findWhere(variable.options, {text: variable.current.text});
if (currentOption) {
return self.setVariableValue(variable, currentOption, true);
return self.setVariableValue(variable, currentOption, false);
} else {
if (!variable.options.length) { return; }
return self.setVariableValue(variable, variable.options[0]);
@ -313,6 +326,14 @@ function (angular, _, kbn) {
var value = item.value || item.text;
var text = item.text || item.value;
if (_.isNumber(value)) {
value = value.toString();
}
if (_.isNumber(text)) {
text = text.toString();
}
if (regex) {
matches = regex.exec(value);
if (!matches) { continue; }

View File

@ -73,14 +73,5 @@
</div>
</div>
<div class="row" style="margin-top: 50px">
<div class="version-footer text-center small">
Grafana version: {{buildInfo.version}}, commit: {{buildInfo.commit}},
build date: {{buildInfo.buildstamp | date: 'yyyy-MM-dd HH:mm:ss' }}
</div>
<div class="version-footer text-center small" ng-show="buildInfo.hasUpdate">
<a class="external-link" target="_blank" href="http://grafana.org/download">New Grafana Version Available ({{buildInfo.latestVersion}})</a>
</div>
</div>
</div>
</div>

View File

@ -67,7 +67,6 @@
</form>
</div>
</div>
</div>

View File

@ -78,7 +78,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
range[timeField]["format"] = "epoch_millis";
}
var queryInterpolated = templateSrv.replace(queryString);
var queryInterpolated = templateSrv.replace(queryString, {}, 'lucene');
var filter = { "bool": { "must": [{ "range": range }] } };
var query = { "bool": { "should": [{ "query_string": { "query": queryInterpolated } }] } };
var data = {
@ -204,6 +204,14 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
});
};
function escapeForJson(value) {
return value.replace(/\"/g, '\\"');
}
function luceneThenJsonFormat(value) {
return escapeForJson(templateSrv.luceneFormat(value));
}
this.getFields = function(query) {
return this._get('/_mapping').then(function(res) {
var fields = {};
@ -246,7 +254,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
var header = this.getQueryHeader('count', range.from, range.to);
var esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef));
esQuery = esQuery.replace("$lucene_query", queryDef.query || '*');
esQuery = esQuery.replace("$lucene_query", escapeForJson(queryDef.query || '*'));
esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf());
esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf());
esQuery = header + '\n' + esQuery + '\n';
@ -260,7 +268,7 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
};
this.metricFindQuery = function(query) {
query = templateSrv.replace(query);
query = templateSrv.replace(query, {}, luceneThenJsonFormat);
query = angular.fromJson(query);
if (!query) {
return $q.when([]);

View File

@ -70,9 +70,9 @@
</div>
<div ng-if="agg.type === 'filters'">
<div class="gf-form-inline" ng-repeat="filter in agg.settings.filters" ng-class="{last: $last}">
<div class="gf-form-inline offset-width-7" ng-repeat="filter in agg.settings.filters">
<div class="gf-form">
<label class="gf-form-item width-10">Query {{$index + 1}}</label>
<label class="gf-form-label width-10">Query {{$index + 1}}</label>
<input type="text" class="gf-form-input max-width-12" ng-model="filter.query" spellcheck='false' placeholder="Lucene query" ng-blur="onChangeInternal()">
</div>
<div class="gf-form">
@ -88,7 +88,7 @@
<div ng-if="agg.type === 'geohash_grid'">
<div class="gf-form offset-width-7">
<label class="gf-form-label">Precision</label>
<label class="gf-form-label width-10">Precision</label>
<input type="number" class="gf-form-input max-width-12" ng-model="agg.settings.precision" spellcheck='false' placeholder="3" ng-blur="onChangeInternal()">
</div>
</div>

View File

@ -256,23 +256,14 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
return this.renderTemplate(options.legendFormat, labelData) || '{}';
};
this.renderTemplate = function(format, data) {
var originalSettings = _.templateSettings;
_.templateSettings = {
interpolate: /\{\{(.+?)\}\}/g
};
var template = _.template(templateSrv.replace(format));
var result;
try {
result = template(data);
} catch (e) {
result = null;
}
_.templateSettings = originalSettings;
return result;
this.renderTemplate = function(aliasPattern, aliasData) {
var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
return aliasPattern.replace(aliasRegex, function(match, g1) {
if (aliasData[g1]) {
return aliasData[g1];
}
return g1;
});
};
this.getOriginalMetricName = function(labelData) {

View File

@ -58,6 +58,10 @@ class PrometheusQueryCtrl extends QueryCtrl {
updateLink() {
var range = this.panelCtrl.range;
if (!range) {
return;
}
var rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000);
var endTime = range.to.utc().format('YYYY-MM-DD HH:mm');
var expr = {

View File

@ -66,7 +66,7 @@ function (angular, $, moment, _, kbn, GraphTooltip) {
function getLegendHeight(panelHeight) {
if (!panel.legend.show || panel.legend.rightSide) {
return 2;
return 0;
}
if (panel.legend.alignAsTable) {

View File

@ -22,8 +22,8 @@ describe('GraphCtrl', function() {
describe('msResolution with second resolution timestamps', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[1234567890, 45], [1234567899, 60]]},
{ target: 'test.cpu2', datapoints: [[1236547890, 55], [1234456709, 90]]}
{ target: 'test.cpu1', datapoints: [[45, 1234567890], [60, 1234567899]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890], [90, 1234456709]]}
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
@ -37,8 +37,8 @@ describe('GraphCtrl', function() {
describe('msResolution with millisecond resolution timestamps', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
{ target: 'test.cpu2', datapoints: [[1236547890001, 55], [1234456709000, 90]]}
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890001], [90, 1234456709000]]}
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
@ -52,8 +52,8 @@ describe('GraphCtrl', function() {
describe('msResolution with millisecond resolution timestamps but with trailing zeroes', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
{ target: 'test.cpu2', datapoints: [[1236547890000, 55], [1234456709000, 90]]}
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890000], [90, 1234456709000]]}
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);
@ -67,9 +67,9 @@ describe('GraphCtrl', function() {
describe('msResolution with millisecond resolution timestamps in one of the series', function() {
beforeEach(function() {
var data = [
{ target: 'test.cpu1', datapoints: [[1234567890000, 45], [1234567899000, 60]]},
{ target: 'test.cpu2', datapoints: [[1236547890010, 55], [1234456709000, 90]]},
{ target: 'test.cpu3', datapoints: [[1236547890000, 65], [1234456709000, 120]]}
{ target: 'test.cpu1', datapoints: [[45, 1234567890000], [60, 1234567899000]]},
{ target: 'test.cpu2', datapoints: [[55, 1236547890010], [90, 1234456709000]]},
{ target: 'test.cpu3', datapoints: [[65, 1236547890000], [120, 1234456709000]]}
];
ctx.ctrl.panel.tooltip.msResolution = false;
ctx.ctrl.onDataReceived(data);

View File

@ -1,4 +1,3 @@
<div class="singlestat-panel">
</div>
<div class="clearfix"></div>

View File

@ -325,6 +325,9 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
function addGauge() {
var width = elem.width();
var height = elem.height();
ctrl.invalidGaugeRange = false;
if (panel.gauge.minValue > panel.gauge.maxValue) {
ctrl.invalidGaugeRange = true;
@ -332,8 +335,6 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
var plotCanvas = $('<div></div>');
var width = elem.width();
var height = elem.height();
var plotCss = {
top: '10px',
margin: 'auto',

View File

@ -268,3 +268,7 @@ $checkboxImageUrl: '../img/checkbox.png';
$card-background: linear-gradient(135deg, #2f2f2f, #262626);
$card-background-hover: linear-gradient(135deg, #343434, #262626);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3);
// footer
$footer-link-color: $gray-1;
$footer-link-hover: $gray-4;

View File

@ -292,3 +292,7 @@ $checkboxImageUrl: '../img/checkbox_white.png';
$card-background: linear-gradient(135deg, $gray-5, $gray-6);
$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7);
$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1);
// footer
$footer-link-color: $gray-3;
$footer-link-hover: $dark-5;

View File

@ -1,9 +1,38 @@
.grafana-version-info {
position: absolute;
bottom: 2px;
left: 3px;
font-size: 80%;
color: darken($gray-1, 25%);
a { color: darken($gray-1, 25%); }
.page-dashboard .footer {
display: none;
}
.footer {
color: $footer-link-color;
padding: 5rem 0 1rem 0;
font-size: $font-size-xs;
width: 98%; /* was causing horiz scrollbars - need to examine */
a {
color: $footer-link-color;
&:hover {
color: $footer-link-hover;
}
}
ul {
list-style: none;
}
li {
display: inline-block;
padding-right: 2px;
&:after {
content: ' | ';
padding-left: 2px;
}
}
li:last-child {
&:after {
padding-left: 0;
content: '';
}
}
}

View File

@ -5,7 +5,6 @@
}
.singlestat-panel-value-container {
padding: 20px;
display: table-cell;
vertical-align: middle;
text-align: center;

View File

@ -4,7 +4,7 @@
}
.main-view {
height: 100%;
// height: 100%; REMOVED FOR FOOTER TRW
}
.page-container {

View File

@ -56,7 +56,7 @@ define([
});
});
describe('can detect if serie contains ms precision', function() {
describe('can detect if series contains ms precision', function() {
var fakedata;
beforeEach(function() {
@ -64,13 +64,13 @@ define([
});
it('missing datapoint with ms precision', function() {
fakedata.datapoints[0] = [1234567890000, 1337];
fakedata.datapoints[0] = [1337, 1234567890000];
series = new TimeSeries(fakedata);
expect(series.isMsResolutionNeeded()).to.be(false);
});
it('contains datapoint with ms precision', function() {
fakedata.datapoints[0] = [1236547890001, 1337];
fakedata.datapoints[0] = [1337, 1236547890001];
series = new TimeSeries(fakedata);
expect(series.isMsResolutionNeeded()).to.be(true);
});

View File

@ -141,8 +141,8 @@ define([
});
it('slash should be properly escaped in regex format', function() {
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
var result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).to.be('Gi3\\/14');
});
});
@ -200,6 +200,15 @@ define([
expect(contains).to.be(true);
});
it('should find it when part of segment', function() {
var contains = _templateSrv.containsVariable('metrics.$env.$group-*', 'group');
expect(contains).to.be(true);
});
it('should find it its the only thing', function() {
var contains = _templateSrv.containsVariable('$env', 'env');
expect(contains).to.be(true);
});
});
describe('updateTemplateData with simple value', function() {

View File

@ -126,6 +126,59 @@ define([
});
});
describeUpdateVariable('query variable with multi select and new options does not contain some selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val2'}, {text: 'val3'}];
});
it('should update current value', function() {
expect(scenario.variable.current.value).to.eql(['val2', 'val3']);
expect(scenario.variable.current.text).to.eql('val2 + val3');
});
});
describeUpdateVariable('query variable with multi select and new options does not contain any selected values', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
current: {
value: ['val1', 'val2', 'val3'],
text: 'val1 + val2 + val3'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should update current value with first one', function() {
expect(scenario.variable.current.value).to.eql('val5');
expect(scenario.variable.current.text).to.eql('val5');
});
});
describeUpdateVariable('query variable with numeric results', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'query', query: '', name: 'test', current: {} };
scenario.queryResult = [{text: 12, value: 12}];
});
it('should set current value to first option', function() {
expect(scenario.variable.current.value).to.be('12');
expect(scenario.variable.options[0].value).to.be('12');
expect(scenario.variable.options[0].text).to.be('12');
});
});
describeUpdateVariable('interval variable without auto', function(scenario) {
scenario.setup(function() {
scenario.variable = { type: 'interval', query: '1s,2h,5h,1d', name: 'test' };

View File

@ -39,6 +39,41 @@
</div>
<div ng-view class="main-view"></div>
<footer class="footer">
<div class="row text-center">
<ul>
<li>
<a href="http://docs.grafana.org" target="_blank">
<i class="fa fa-file-code-o"></i>
Docs
</a>
</li>
<li>
<a href="https://grafana.net/support/plans" target="_blank">
<i class="fa fa-support"></i>
Support Plans
</a>
</li>
<li>
<a href="https://grafana.org/community" target="_blank">
<i class="fa fa-comments-o"></i>
Community
</a>
</li>
<li>
<a href="http://grafana.org" target="_blank">Grafana</a>
<span>v[[.BuildVersion]] (commit: [[.BuildCommit]])</span>
</li>
<li>
[[if .NewGrafanaVersionExists]]
<a href="http://grafana.org/download" target="_blank" bs-tooltip="'[[.NewGrafanaVersion]]'">
New version available!
</a>
[[end]]
</li>
</ul>
</div>
</footer>
</grafana-app>
</body>

View File

@ -38,10 +38,10 @@
function checkIsReady() {
var canvas = page.evaluate(function() {
if (!window.angular) { return false; }
var body = window.angular.element(document.body); // 1
var body = window.angular.element(document.body);
if (!body.scope) { return false; }
var rootScope = body.scope();
var rootScope = body.injector().get('$rootScope');
if (!rootScope) {return false;}
if (!rootScope.performance) { return false; }
var panelsToLoad = window.angular.element('div.panel').length;