Merge branch 'master' into alerting_definitions

This commit is contained in:
bergquist 2016-05-30 08:26:08 +02:00
commit 1686d86c3b
68 changed files with 768 additions and 219 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

@ -0,0 +1,16 @@
FROM ubuntu:xenial
ENV DEBIAN_FRONTEND noninteractive
RUN apt-get -y update
RUN apt-get -y install collectd curl python-pip
# add a fake mtab for host disk stats
ADD etc_mtab /etc/mtab
ADD collectd.conf.tpl /etc/collectd/collectd.conf.tpl
RUN pip install envtpl
ADD start_container /usr/bin/start_container
RUN chmod +x /usr/bin/start_container
CMD start_container

View File

@ -0,0 +1,37 @@
collectd-write-graphite
=======================
Basic collectd-based server monitoring. Sends stats to Graphite.
Collectd metrics:
* CPU used/free/idle/etc
* Free disk (via mounting hosts '/' into container, eg: -v /:/hostfs:ro)
* Disk performance
* Load average
* Memory used/free/etc
* Uptime
* Network interface
* Swap
Environment variables
---------------------
* `HOST_NAME`
- Will be sent to Graphite
- Required
* `GRAPHITE_HOST`
- Graphite IP or hostname
- Required
* `GRAPHITE_PORT`
- Graphite port
- Optional, defaults to 2003
* `GRAPHITE_PREFIX`
- Graphite prefix
- Optional, defaults to collectd.
* `REPORT_BY_CPU`
- Report per-CPU metrics if true, global sum of CPU metrics if false (details: [collectd.conf man page](https://collectd.org/documentation/manpages/collectd.conf.5.shtml#plugin_cpu))
- Optional, defaults to false.
* `COLLECT_INTERVAL`
- Collection interval and thus resolution of metrics
- Optional, defaults to 10

View File

@ -0,0 +1,76 @@
Hostname "{{ HOST_NAME }}"
FQDNLookup false
Interval {{ COLLECT_INTERVAL | default("10") }}
Timeout 2
ReadThreads 5
LoadPlugin cpu
LoadPlugin df
LoadPlugin load
LoadPlugin memory
LoadPlugin disk
LoadPlugin interface
LoadPlugin uptime
LoadPlugin swap
LoadPlugin write_graphite
<Plugin cpu>
ReportByCpu {{ REPORT_BY_CPU | default("false") }}
</Plugin>
<Plugin df>
# expose host's mounts into container using -v /:/host:ro (location inside container does not matter much)
# ignore rootfs; else, the root file-system would appear twice, causing
# one of the updates to fail and spam the log
FSType rootfs
# ignore the usual virtual / temporary file-systems
FSType sysfs
FSType proc
FSType devtmpfs
FSType devpts
FSType tmpfs
FSType fusectl
FSType cgroup
FSType overlay
FSType debugfs
FSType pstore
FSType securityfs
FSType hugetlbfs
FSType squashfs
FSType mqueue
MountPoint "/etc/resolv.conf"
MountPoint "/etc/hostname"
MountPoint "/etc/hosts"
IgnoreSelected true
ReportByDevice false
ReportReserved true
ReportInodes true
</Plugin>
<Plugin "disk">
Disk "/^[hs]d[a-z]/"
IgnoreSelected false
</Plugin>
<Plugin interface>
Interface "lo"
Interface "/^veth.*/"
Interface "/^docker.*/"
IgnoreSelected true
</Plugin>
<Plugin "write_graphite">
<Carbon>
Host "{{ GRAPHITE_HOST }}"
Port "{{ GRAPHITE_PORT | default("2003") }}"
Prefix "{{ GRAPHITE_PREFIX | default("collectd.") }}"
EscapeCharacter "_"
SeparateInstances true
StoreRates true
AlwaysAppendDS false
</Carbon>
</Plugin>

View File

@ -0,0 +1 @@
hostfs /.dockerinit ext4 ro,relatime,user_xattr,barrier=1,data=ordered 0 0

View File

@ -0,0 +1,11 @@
collectd:
build: blocks/collectd
environment:
HOST_NAME: myserver
GRAPHITE_HOST: graphite
GRAPHITE_PORT: 2003
GRAPHITE_PREFIX: collectd.
REPORT_BY_CPU: 'false'
COLLECT_INTERVAL: 10
links:
- graphite

View File

@ -0,0 +1,5 @@
#!/bin/bash
envtpl /etc/collectd/collectd.conf.tpl
collectd -f

View File

@ -1,7 +1,15 @@
To build the docs locally, you need to have docker installed. The docs are built using a custom [docker](https://www.docker.com/)
image and [mkdocs](http://www.mkdocs.org/).
# Building The Docs
Build the `grafana/docs-base:latest` image:
To build the docs locally, you need to have docker installed. The
docs are built using a custom [docker](https://www.docker.com/) image
and the [mkdocs](http://www.mkdocs.org/) tool.
**Prepare the Docker Image**:
Build the `grafana/docs-base:latest` image. Run these commands in the
same directory this file is in. **Note** that you may require ``sudo``
when running ``make docs-build`` depending on how your system's docker
service is configured):
```
$ git clone https://github.com/grafana/docs-base
@ -9,10 +17,45 @@ $ cd docs-base
$ make docs-build
```
To build the docs:
**Build the Documentation**:
Now that the docker image has been prepared we can build the
docs. Switch your working directory back to the directory this file
(README.md) is in and run (possibly with ``sudo``):
```
$ cd docs
$ make docs
```
This command will not return control of the shell to the user. Instead
the command is now running a new docker container built from the image
we created in the previous step.
Open [localhost:8180](http://localhost:8180) to view the docs.
**Note** that after running ``make docs`` you may notice a message
like this in the console output
> Running at: http://0.0.0.0:8000/
This is misleading. That is **not** the port the documentation is
served from. You must browse to port **8180** to view the new
documentation.
# Adding a New Page
Adding a new page requires updating the ``mkdocs.yml`` file which is
located in this directory.
For example, if you are adding documentation for a new HTTP API called
``preferences`` you would:
1. Create the file ``docs/sources/http_api/preferences.md``
1. Add a reference to it in ``docs/sources/http_api/overview.md``
1. Update the list under the **pages** key in the ``docs/mkdocs.yml`` file with a reference to your new page:
```yaml
- ['http_api/preferences.md', 'API', 'Preferences API']
```

View File

@ -84,6 +84,7 @@ pages:
- ['http_api/user.md', 'API', 'User API']
- ['http_api/admin.md', 'API', 'Admin API']
- ['http_api/snapshot.md', 'API', 'Snapshot API']
- ['http_api/preferences.md', 'API', 'Preferences API']
- ['http_api/other.md', 'API', 'Other API']
- ['plugins/index.md', 'Plugins', 'Overview']

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

@ -20,7 +20,7 @@
"grunt-angular-templates": "^0.5.5",
"grunt-cli": "~0.1.13",
"grunt-contrib-clean": "~0.7.0",
"grunt-contrib-compress": "~0.14.0",
"grunt-contrib-compress": "^1.3.0",
"grunt-contrib-concat": "^0.5.1",
"grunt-contrib-copy": "~0.8.2",
"grunt-contrib-cssmin": "~0.14.0",

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

@ -117,6 +117,7 @@ func Register(r *macaron.Macaron) {
r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
r.Post("/:id/using/:orgId", wrap(UpdateUserActiveOrg))
}, reqGrafanaAdmin)
// org information available to all users.
@ -211,7 +212,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

@ -45,6 +45,15 @@ func init() {
"AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps"},
"AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"},
"AWS/ELB": {"HealthyHostCount", "UnHealthyHostCount", "RequestCount", "Latency", "HTTPCode_ELB_4XX", "HTTPCode_ELB_5XX", "HTTPCode_Backend_2XX", "HTTPCode_Backend_3XX", "HTTPCode_Backend_4XX", "HTTPCode_Backend_5XX", "BackendConnectionErrors", "SurgeQueueLength", "SpilloverCount"},
"AWS/ElasticBeanstalk": {
"EnvironmentHealth",
"ApplicationLatencyP10", "ApplicationLatencyP50", "ApplicationLatencyP75", "ApplicationLatencyP85", "ApplicationLatencyP90", "ApplicationLatencyP95", "ApplicationLatencyP99", "ApplicationLatencyP99.9",
"ApplicationRequests2xx", "ApplicationRequests3xx", "ApplicationRequests4xx", "ApplicationRequests5xx", "ApplicationRequestsTotal",
"CPUIdle", "CPUIowait", "CPUIrq", "CPUNice", "CPUSoftirq", "CPUSystem", "CPUUser",
"InstanceHealth", "InstancesDegraded", "InstancesInfo", "InstancesNoData", "InstancesOk", "InstancesPending", "InstancesSevere", "InstancesUnknown", "InstancesWarning",
"LoadAverage1min", "LoadAverage5min",
"RootFilesystemUtil",
},
"AWS/ElasticMapReduce": {"IsIdle", "JobsRunning", "JobsFailed",
"MapTasksRunning", "MapTasksRemaining", "MapSlotsOpen", "RemainingMapTasksPerSlot", "ReduceTasksRunning", "ReduceTasksRemaining", "ReduceSlotsOpen",
"CoreNodesRunning", "CoreNodesPending", "LiveDataNodes", "TaskNodesRunning", "TaskNodesPending", "LiveTaskTrackers",
@ -85,6 +94,7 @@ func init() {
"AWS/EBS": {"VolumeId"},
"AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"},
"AWS/ELB": {"LoadBalancerName", "AvailabilityZone"},
"AWS/ElasticBeanstalk": {"EnvironmentName", "InstanceId"},
"AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"},
"AWS/ES": {"ClientId", "DomainName"},
"AWS/Events": {"RuleName"},

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"
@ -174,30 +175,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}
c.JSON(200, &dashRedirect)
return
return Json(200, &dashRedirect)
} else {
log.Warn("Failed to get slug from database, %s", err.Error())
}
}
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{}
@ -205,11 +203,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

@ -55,6 +55,13 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht
req.Header.Add("Authorization", util.GetBasicAuthHeader(ds.BasicAuthUser, ds.BasicAuthPassword))
}
dsAuth := req.Header.Get("X-DS-Authorization")
if len(dsAuth) > 0 {
req.Header.Del("X-DS-Authorization")
req.Header.Del("Authorization")
req.Header.Add("Authorization", dsAuth)
}
// clear cookie headers
req.Header.Del("Cookie")
req.Header.Del("Set-Cookie")

View File

@ -8,6 +8,10 @@ type IndexViewData struct {
GoogleAnalyticsId string
GoogleTagManagerId string
MainNavLinks []*NavLink
BuildVersion string
BuildCommit string
NewGrafanaVersionExists bool
NewGrafanaVersion string
}
type PluginCss struct {

View File

@ -41,6 +41,10 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) {
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

@ -40,6 +40,24 @@ func UpdateUser(c *middleware.Context, cmd m.UpdateUserCommand) Response {
return handleUpdateUser(cmd)
}
//POST /api/users/:id/using/:orgId
func UpdateUserActiveOrg(c *middleware.Context) Response {
userId := c.ParamsInt64(":id")
orgId := c.ParamsInt64(":orgId")
if !validateUsingOrg(userId, orgId) {
return ApiError(401, "Not a valid organization", nil)
}
cmd := m.SetUsingOrgCommand{UserId: userId, OrgId: orgId}
if err := bus.Dispatch(&cmd); err != nil {
return ApiError(500, "Failed change active organization", err)
}
return ApiSuccess("Active organization changed")
}
func handleUpdateUser(cmd m.UpdateUserCommand) Response {
if len(cmd.Login) == 0 {
cmd.Login = cmd.Email

View File

@ -25,7 +25,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,8 +209,10 @@ function (_, $, coreModule) {
// needs to call this after digest so
// property is synced with outerscope
$scope.$$postDigest(function() {
$scope.$apply(function() {
$scope.onChange();
});
});
};
$scope.segment = $scope.valueToSegment($scope.property);

View File

@ -244,7 +244,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
registerPluginComponent(scope, elem, attrs, componentInfo);
}).catch(err => {
$rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]);
console.log('Plugin componnet error', err);
console.log('Plugin component error', err);
});
}
};

View File

@ -96,6 +96,11 @@ function (angular, _, coreModule, config) {
var requestIsLocal = options.url.indexOf('/') === 0;
var firstAttempt = options.retry === 0;
if (requestIsLocal && options.headers && options.headers.Authorization) {
options.headers['X-DS-Authorization'] = options.headers.Authorization;
delete options.headers.Authorization;
}
return $http(options).then(null, function(err) {
// handle unauthorized for backend requests
if (requestIsLocal && firstAttempt && err.status === 401) {

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

@ -12,9 +12,21 @@ function($, _) {
kbn.round_interval = function(interval) {
switch (true) {
// 0.3s
case (interval <= 300):
// 0.015s
case (interval <= 15):
return 10; // 0.01s
// 0.035s
case (interval <= 35):
return 20; // 0.02s
// 0.075s
case (interval <= 75):
return 50; // 0.05s
// 0.15s
case (interval <= 150):
return 100; // 0.1s
// 0.35s
case (interval <= 350):
return 200; // 0.2s
// 0.75s
case (interval <= 750):
return 500; // 0.5s
@ -133,7 +145,7 @@ function($, _) {
return str;
};
kbn.interval_regex = /(\d+(?:\.\d+)?)([Mwdhmsy])/;
kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
// histogram & trends
kbn.intervals_in_seconds = {
@ -143,7 +155,8 @@ function($, _) {
d: 86400,
h: 3600,
m: 60,
s: 1
s: 1,
ms: 0.001
};
kbn.calculateInterval = function(range, resolution, userInterval) {

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

@ -70,12 +70,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, private dynamicDashboardSrv) {
constructor(private $rootScope,
private templateValuesSrv,
private templateSrv,
private dynamicDashboardSrv,
private $location) {
this.annotations = this.dashboard.templating.list;
this.variables = this.dashboard.templating.list;
}

View File

@ -8,7 +8,7 @@ function (angular, _, $) {
var module = angular.module('grafana.services');
module.factory('dashboardViewStateSrv', function($location, $timeout) {
module.factory('dashboardViewStateSrv', function($location, $timeout, templateSrv, contextSrv, timeSrv) {
// represents the transient view state
// like fullscreen panel & edit
@ -25,6 +25,19 @@ function (angular, _, $) {
}
};
// update url on time range change
$scope.onAppEvent('time-range-changed', function() {
var urlParams = $location.search();
var urlRange = timeSrv.timeRangeForUrl();
urlParams.from = urlRange.from;
urlParams.to = urlRange.to;
$location.search(urlParams);
});
$scope.onAppEvent('template-variable-value-updated', function() {
self.updateUrlParamsWithCurrentVariables();
});
$scope.onAppEvent('$routeUpdate', function() {
var urlState = self.getQueryStringState();
if (self.needsSync(urlState)) {
@ -40,10 +53,26 @@ function (angular, _, $) {
self.registerPanel(payload.scope);
});
this.update(this.getQueryStringState(), true);
this.update(this.getQueryStringState());
this.expandRowForPanel();
}
DashboardViewState.prototype.updateUrlParamsWithCurrentVariables = function() {
// update url
var params = $location.search();
// remove variable params
_.each(params, function(value, key) {
if (key.indexOf('var-') === 0) {
delete params[key];
}
});
// add new values
templateSrv.fillVariableValuesForUrl(params);
// update url
$location.search(params);
};
DashboardViewState.prototype.expandRowForPanel = function() {
if (!this.state.panelId) { return; }
@ -63,6 +92,7 @@ function (angular, _, $) {
state.fullscreen = state.fullscreen ? true : null;
state.edit = (state.edit === "true" || state.edit === true) || null;
state.editview = state.editview || null;
state.org = contextSrv.user.orgId;
return state;
};
@ -70,10 +100,11 @@ function (angular, _, $) {
var urlState = _.clone(this.state);
urlState.fullscreen = this.state.fullscreen ? true : null;
urlState.edit = this.state.edit ? true : null;
urlState.org = contextSrv.user.orgId;
return urlState;
};
DashboardViewState.prototype.update = function(state, skipUrlSync) {
DashboardViewState.prototype.update = function(state) {
_.extend(this.state, state);
this.dashboard.meta.fullscreen = this.state.fullscreen;
@ -83,10 +114,7 @@ function (angular, _, $) {
this.state.edit = null;
}
if (!skipUrlSync) {
$location.search(this.serializeToUrl());
}
this.syncState();
};

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';
@ -158,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

@ -1,9 +1,5 @@
<div class="main">
<div class="row-fluid">
<div class="span12">
<div class="panel nospace" ng-if="panel" style="width: 100%">
<div class="panel nospace" ng-if="panel" style="width: 100%">
<plugin-component type="panel">
</plugin-component>
</div>
</div>
</div>
<div class="clearfix"></div>

View File

@ -17,6 +17,10 @@ function (angular, $) {
var params = $location.search();
panelId = parseInt(params.panelId);
// add fullscreen param;
params.fullscreen = true;
$location.search(params);
dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) {
$scope.initDashboard(result, $scope);
});

View File

@ -25,6 +25,7 @@ function (angular, _) {
{value: "interval", text: "Interval"},
{value: "datasource", text: "Data source"},
{value: "custom", text: "Custom"},
{value: "constant", text: "Constant"},
];
$scope.refreshOptions = [
@ -141,15 +142,34 @@ function (angular, _) {
$scope.current = angular.copy(replacementDefaults);
};
$scope.showSelectionOptions = function() {
if ($scope.current) {
if ($scope.current.type === 'query') {
return true;
}
if ($scope.current.type === 'custom') {
return true;
}
}
return false;
};
$scope.typeChanged = function () {
if ($scope.current.type === 'interval') {
$scope.current.query = '1m,10m,30m,1h,6h,12h,1d,7d,14d,30d';
$scope.current.refresh = 0;
}
if ($scope.current.type === 'query') {
$scope.current.query = '';
}
if ($scope.current.type === 'constant') {
$scope.current.query = '';
$scope.current.refresh = 0;
$scope.current.hide = 2;
}
if ($scope.current.type === 'datasource') {
$scope.current.query = $scope.datasourceTypes[0].value;
$scope.current.regex = '';

View File

@ -152,6 +152,14 @@
</div>
</div>
<div ng-show="current.type === 'constant'" class="gf-form-group">
<h5 class="section-heading">Constant options</h5>
<div class="gf-form">
<span class="gf-form-label">Value</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="your metric prefix"></input>
</div>
</div>
<div ng-show="current.type === 'query'" class="gf-form-group">
<h5 class="section-heading">Query Options</h5>
@ -214,7 +222,7 @@
</div>
</div>
<div class="section gf-form-group" ng-hide="current.type === 'datasource'">
<div class="section gf-form-group" ng-show="showSelectionOptions()">
<h5 class="section-heading">Selection Options</h5>
<div class="section">
<gf-form-switch class="gf-form"

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);
@ -125,8 +127,8 @@ function (angular, _, kbn) {
this.setVariableValue = function(variable, option, initPhase) {
variable.current = angular.copy(option);
if (_.isArray(variable.current.value)) {
variable.current.text = variable.current.value.join(' + ');
if (_.isArray(variable.current.text)) {
variable.current.text = variable.current.text.join(' + ');
}
self.selectOptionsForCurrentValue(variable);
@ -166,6 +168,11 @@ function (angular, _, kbn) {
return;
}
if (variable.type === 'constant') {
variable.options = [{text: variable.query, value: variable.query}];
return;
}
// extract options in comma seperated string
variable.options = _.map(variable.query.split(/[,]+/), function(text) {
return { text: text.trim(), value: text.trim() };
@ -173,6 +180,7 @@ function (angular, _, kbn) {
if (variable.type === 'interval') {
self.updateAutoInterval(variable);
return;
}
if (variable.type === 'custom' && variable.includeAll) {
@ -224,6 +232,7 @@ function (angular, _, kbn) {
this.selectOptionsForCurrentValue = function(variable) {
var i, y, value, option;
var selected = [];
for (i = 0; i < variable.options.length; i++) {
option = variable.options[i];
@ -233,28 +242,44 @@ function (angular, _, kbn) {
value = variable.current.value[y];
if (option.value === value) {
option.selected = true;
selected.push(option);
}
}
} else if (option.value === variable.current.value) {
option.selected = true;
selected.push(option);
}
}
return selected;
};
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);
var selected = self.selectOptionsForCurrentValue(variable);
// if none pick first
if (selected.length === 0) {
selected = variable.options[0];
} else {
selected = {
value: _.map(selected, function(val) {return val.value;}),
text: _.map(selected, function(val) {return val.text;}).join(' + '),
};
}
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; }
if (!variable.options.length) { return $q.when(null); }
return self.setVariableValue(variable, variable.options[0]);
}
}
@ -313,6 +338,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;
this.renderTemplate = function(aliasPattern, aliasData) {
var aliasRegex = /\{\{\s*(.+?)\s*\}\}/g;
return aliasPattern.replace(aliasRegex, function(match, g1) {
if (aliasData[g1]) {
return aliasData[g1];
}
_.templateSettings = originalSettings;
return result;
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

@ -269,3 +269,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

@ -293,3 +293,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

@ -147,5 +147,11 @@ define([
var str = kbn.calculateInterval(range, 1000, '>10s');
expect(str).to.be('20m');
});
it('10s 900 resolution and user low limit in ms', function() {
var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
var str = kbn.calculateInterval(range, 900, '>15ms');
expect(str).to.be('15ms');
});
});
});

View File

@ -5,8 +5,20 @@ define([
describe('when updating view state', function() {
var viewState, location;
var timeSrv = {};
var templateSrv = {};
var contextSrv = {
user: {
orgId: 19
}
};
beforeEach(module('grafana.services'));
beforeEach(module(function($provide) {
$provide.value('timeSrv', timeSrv);
$provide.value('templateSrv', templateSrv);
$provide.value('contextSrv', contextSrv);
}));
beforeEach(inject(function(dashboardViewStateSrv, $location, $rootScope) {
$rootScope.onAppEvent = function() {};
@ -17,9 +29,9 @@ define([
describe('to fullscreen true and edit true', function() {
it('should update querystring and view state', function() {
var updateState = { fullscreen: true, edit: true, panelId: 1 };
var updateState = {fullscreen: true, edit: true, panelId: 1};
viewState.update(updateState);
expect(location.search()).to.eql(updateState);
expect(location.search()).to.eql({fullscreen: true, edit: true, panelId: 1, org: 19});
expect(viewState.dashboard.meta.fullscreen).to.be(true);
expect(viewState.state.fullscreen).to.be(true);
});
@ -29,7 +41,7 @@ define([
it('should remove params from query string', function() {
viewState.update({fullscreen: true, panelId: 1, edit: true});
viewState.update({fullscreen: false});
expect(location.search()).to.eql({});
expect(location.search()).to.eql({org: 19});
expect(viewState.dashboard.meta.fullscreen).to.be(false);
expect(viewState.state.fullscreen).to.be(null);
});

View File

@ -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,80 @@ 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 multi select and $__all selected', function(scenario) {
scenario.setup(function() {
scenario.variable = {
type: 'query',
query: '',
name: 'test',
includeAll: true,
current: {
value: ['$__all'],
text: 'All'
}
};
scenario.queryResult = [{text: 'val5'}, {text: 'val6'}];
});
it('should keep current All value', function() {
expect(scenario.variable.current.value).to.eql(['$__all']);
expect(scenario.variable.current.text).to.eql('All');
});
});
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,11 @@
function checkIsReady() {
var canvas = page.evaluate(function() {
if (!window.angular) { return false; }
var body = window.angular.element(document.body); // 1
if (!body.scope) { return false; }
var body = window.angular.element(document.body);
if (!body.injector) { return false; }
if (!body.injector()) { 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;
@ -59,6 +60,7 @@
width: bb.width,
height: bb.height
};
page.render(params.png);
phantom.exit();
}