mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
merge with master
This commit is contained in:
commit
c4fede6cf6
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,13 +9,12 @@ awsconfig
|
||||
/public/vendor/npm
|
||||
/tmp
|
||||
vendor/phantomjs/phantomjs
|
||||
vendor/phantomjs/phantomjs.exe
|
||||
|
||||
docs/AWS_S3_BUCKET
|
||||
docs/GIT_BRANCH
|
||||
docs/VERSION
|
||||
docs/GITCOMMIT
|
||||
docs/changed-files
|
||||
docs/changed-files
|
||||
|
||||
# locally required config files
|
||||
public/css/*.min.css
|
||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -1,4 +1,10 @@
|
||||
# 4.4.0 (unreleased)
|
||||
# 4.4.1 (2017-07-05)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **Migrations**: migration fails where dashboard.created_by is null [#8783](https://github.com/grafana/grafana/issues/8783)
|
||||
|
||||
# 4.4.0 (2017-07-04)
|
||||
|
||||
## New Features
|
||||
**Dashboard History**: View dashboard version history, compare any two versions (summary & json diffs), restore to old version. This big feature
|
||||
@ -10,6 +16,16 @@ Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472)
|
||||
* **Elasticsearch**: Added filter aggregation label [#8420](https://github.com/grafana/grafana/pull/8420), thx [@tianzk](github.com/tianzk)
|
||||
* **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller)
|
||||
* **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo)
|
||||
* **Table Panel**: Column style that preserves formatting/indentation (like pre tag) [#6617](https://github.com/grafana/grafana/issues/6617)
|
||||
* **DingDing**: Add DingDing Alert Notifier [#8473](https://github.com/grafana/grafana/pull/8473) thx [@jiamliang](https://github.com/jiamliang)
|
||||
|
||||
## Minor Enhancements
|
||||
|
||||
* **Elasticsearch**: Add option for result set size in raw_document [#3426](https://github.com/grafana/grafana/issues/3426) [#8527](https://github.com/grafana/grafana/pull/8527), thx [@mk-dhia](github.com/mk-dhia)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **Graph**: Bug fix for negative values in histogram mode [#8628](https://github.com/grafana/grafana/issues/8628)
|
||||
|
||||
# 4.3.2 (2017-05-31)
|
||||
|
||||
|
@ -18,6 +18,7 @@ Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
|
||||
- [What's New in Grafana 4.1](http://docs.grafana.org/guides/whats-new-in-v4-1/)
|
||||
- [What's New in Grafana 4.2](http://docs.grafana.org/guides/whats-new-in-v4-2/)
|
||||
- [What's New in Grafana 4.3](http://docs.grafana.org/guides/whats-new-in-v4-3/)
|
||||
- [What's New in Grafana 4.4](http://docs.grafana.org/guides/whats-new-in-v4-4/)
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -32,6 +32,7 @@ build_script:
|
||||
- grunt release
|
||||
- go run build.go sha-dist
|
||||
- cp dist/* .
|
||||
- go test -v ./pkg/...
|
||||
|
||||
artifacts:
|
||||
- path: grafana-*windows-*.*
|
||||
|
10
build.go
10
build.go
@ -95,7 +95,9 @@ func main() {
|
||||
|
||||
case "package":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
createLinuxPackages()
|
||||
if runtime.GOOS != "windows" {
|
||||
createLinuxPackages()
|
||||
}
|
||||
|
||||
case "pkg-rpm":
|
||||
grunt(gruntBuildArg("release")...)
|
||||
@ -345,7 +347,11 @@ func ChangeWorkingDir(dir string) {
|
||||
}
|
||||
|
||||
func grunt(params ...string) {
|
||||
runPrint("./node_modules/.bin/grunt", params...)
|
||||
if runtime.GOOS == "windows" {
|
||||
runPrint(`.\node_modules\.bin\grunt`, params...)
|
||||
} else {
|
||||
runPrint("./node_modules/.bin/grunt", params...)
|
||||
}
|
||||
}
|
||||
|
||||
func gruntBuildArg(task string) []string {
|
||||
|
@ -112,6 +112,8 @@ Grafana also supports the following Notification Channels:
|
||||
|
||||
- LINE
|
||||
|
||||
- DingDing
|
||||
|
||||
# Enable images in notifications {#external-image-store}
|
||||
|
||||
Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessable (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
|
||||
|
@ -88,8 +88,8 @@ You can switch to raw query mode by clicking hamburger icon and then `Switch edi
|
||||
- $m = replaced with measurement name
|
||||
- $measurement = replaced with measurement name
|
||||
- $col = replaced with column name
|
||||
- $tag_hostname = replaced with the value of the hostname tag
|
||||
- You can also use [[tag_hostname]] pattern replacement syntax
|
||||
- $tag_exampletag = replaced with the value of the `exampletag` tag. To use your tag as an alias in the ALIAS BY field then the tag must be used to group by in the query.
|
||||
- You can also use [[tag_hostname]] pattern replacement syntax. For example, in the ALIAS BY field using this text `Host: [[tag_hostname]]` would substitute in the `hostname` tag value for each legend value and an example legend value would be: `Host: server1`.
|
||||
|
||||
### Table query / raw data
|
||||
|
||||
|
@ -46,7 +46,8 @@ Name | Description
|
||||
*Query expression* | Prometheus query expression, check out the [Prometheus documentation](http://prometheus.io/docs/querying/basics/).
|
||||
*Legend format* | Controls the name of the time series, using name or pattern. For example `{{hostname}}` will be replaced with label value for the label `hostname`.
|
||||
*Min step* | Set a lower limit for the Prometheus step option. Step controls how big the jumps are when the Prometheus query engine performs range queries. Sadly there is no official prometheus documentation to link to for this very important option.
|
||||
*Resolution* | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels.*Metric lookup* | Search for metric names in this input field.
|
||||
*Resolution* | Controls the step option. Small steps create high-resolution graphs but can be slow over larger time ranges, lowering the resolution can speed things up. `1/2` will try to set step option to generate 1 data point for every other pixel. A value of `1/10` will try to set step option so there is a data point every 10 pixels.
|
||||
*Metric lookup* | Search for metric names in this input field.
|
||||
*Format as* | **(New in v4.3)** Switch between Table & Time series. Table format will only work in the Table panel.
|
||||
|
||||
## Templating
|
||||
|
50
docs/sources/guides/whats-new-in-v4-4.md
Normal file
50
docs/sources/guides/whats-new-in-v4-4.md
Normal file
@ -0,0 +1,50 @@
|
||||
+++
|
||||
title = "What's New in Grafana v4.4"
|
||||
description = "Feature & improvement highlights for Grafana v4.4"
|
||||
keywords = ["grafana", "new", "documentation", "4.4.0"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Version 4.4"
|
||||
identifier = "v4.4"
|
||||
parent = "whatsnew"
|
||||
weight = -3
|
||||
+++
|
||||
|
||||
## What's New in Grafana v4.4
|
||||
|
||||
Grafana v4.4 is now [available for download](https://grafana.com/grafana/download/4.4.0).
|
||||
|
||||
**Highlights**:
|
||||
|
||||
- Dashboard History - version control for dashboards.
|
||||
|
||||
## New Features
|
||||
|
||||
**Dashboard History**: View dashboard version history, compare any two versions (summary & json diffs), restore to old version. This big feature
|
||||
was contributed by **Walmart Labs**. Big thanks to them for this massive contribution!
|
||||
Initial feature request: [#4638](https://github.com/grafana/grafana/issues/4638)
|
||||
Pull Request: [#8472](https://github.com/grafana/grafana/pull/8472)
|
||||
|
||||
## Enhancements
|
||||
* **Elasticsearch**: Added filter aggregation label [#8420](https://github.com/grafana/grafana/pull/8420), thx [@tianzk](github.com/tianzk)
|
||||
* **Sensu**: Added option for source and handler [#8405](https://github.com/grafana/grafana/pull/8405), thx [@joemiller](github.com/joemiller)
|
||||
* **CSV**: Configurable csv export datetime format [#8058](https://github.com/grafana/grafana/issues/8058), thx [@cederigo](github.com/cederigo)
|
||||
* **Table Panel**: Column style that preserves formatting/indentation (like pre tag) [#6617](https://github.com/grafana/grafana/issues/6617)
|
||||
* **DingDing**: Add DingDing Alert Notifier [#8473](https://github.com/grafana/grafana/pull/8473) thx [@jiamliang](https://github.com/jiamliang)
|
||||
|
||||
## Minor Enhancements
|
||||
|
||||
* **Elasticsearch**: Add option for result set size in raw_document [#3426](https://github.com/grafana/grafana/issues/3426) [#8527](https://github.com/grafana/grafana/pull/8527), thx [@mk-dhia](github.com/mk-dhia)
|
||||
|
||||
## Bug Fixes
|
||||
|
||||
* **Graph**: Bug fix for negative values in histogram mode [#8628](https://github.com/grafana/grafana/issues/8628)
|
||||
|
||||
## Download
|
||||
|
||||
Head to the [v4.4 download page](https://grafana.com/grafana/download) for download links & instructions.
|
||||
|
||||
## Thanks
|
||||
|
||||
A big thanks to all the Grafana users who contribute by submitting PRs, bug reports, helping out on our [community site](https://community.grafana.com/) and providing feedback!
|
||||
|
321
docs/sources/http_api/dashboard_versions.md
Normal file
321
docs/sources/http_api/dashboard_versions.md
Normal file
@ -0,0 +1,321 @@
|
||||
+++
|
||||
title = "Dashboard Versions HTTP API "
|
||||
description = "Grafana Dashboard Versions HTTP API"
|
||||
keywords = ["grafana", "http", "documentation", "api", "dashboard", "versions"]
|
||||
aliases = ["/http_api/dashboardversions/"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Dashboard Versions"
|
||||
parent = "http_api"
|
||||
+++
|
||||
|
||||
# Dashboard Versions
|
||||
|
||||
## Get all dashboard versions
|
||||
|
||||
Query parameters:
|
||||
|
||||
- **limit** - Maximum number of results to return
|
||||
- **start** - Version to start from when returning queries
|
||||
|
||||
`GET /api/dashboards/id/:dashboardId/versions`
|
||||
|
||||
Gets all existing dashboard versions for the dashboard with the given `dashboardId`.
|
||||
|
||||
**Example request for getting all dashboard versions**:
|
||||
|
||||
```http
|
||||
GET /api/dashboards/id/1/versions?limit=2?start=0 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example Response**
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 428
|
||||
|
||||
[
|
||||
{
|
||||
"id": 2,
|
||||
"dashboardId": 1,
|
||||
"parentVersion": 1,
|
||||
"restoredFrom": 0,
|
||||
"version": 2,
|
||||
"created": "2017-06-08T17:24:33-04:00",
|
||||
"createdBy": "admin",
|
||||
"message": "Updated panel title"
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"dashboardId": 1,
|
||||
"parentVersion": 0,
|
||||
"restoredFrom": 0,
|
||||
"version": 1,
|
||||
"created": "2017-06-08T17:23:33-04:00",
|
||||
"createdBy": "admin",
|
||||
"message": "Initial save"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Errors
|
||||
- **401** - Unauthorized
|
||||
- **404** - Dashboard version not found
|
||||
|
||||
## Get dashboard version
|
||||
|
||||
`GET /api/dashboards/id/:dashboardId/versions/:id`
|
||||
|
||||
Get the dashboard version with the given id, for the dashboard with the given id.
|
||||
|
||||
**Example request for getting a dashboard version**:
|
||||
|
||||
```http
|
||||
GET /api/dashboards/id/1/versions/1 HTTP/1.1
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
```
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 1300
|
||||
|
||||
{
|
||||
"id": 1,
|
||||
"dashboardId": 1,
|
||||
"parentVersion": 0,
|
||||
"restoredFrom": 0,
|
||||
"version": 1,
|
||||
"created": "2017-04-26T17:18:38-04:00",
|
||||
"message": "Initial save",
|
||||
"data": {
|
||||
"annotations": {
|
||||
"list": [
|
||||
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"gnetId": null,
|
||||
"graphTooltip": 0,
|
||||
"hideControls": false,
|
||||
"id": 1,
|
||||
"links": [
|
||||
|
||||
],
|
||||
"rows": [
|
||||
{
|
||||
"collapse": false,
|
||||
"height": "250px",
|
||||
"panels": [
|
||||
|
||||
],
|
||||
"repeat": null,
|
||||
"repeatIteration": null,
|
||||
"repeatRowId": null,
|
||||
"showTitle": false,
|
||||
"title": "Dashboard Row",
|
||||
"titleSize": "h6"
|
||||
}
|
||||
],
|
||||
"schemaVersion": 14,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
|
||||
],
|
||||
"templating": {
|
||||
"list": [
|
||||
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-6h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": [
|
||||
"5s",
|
||||
"10s",
|
||||
"30s",
|
||||
"1m",
|
||||
"5m",
|
||||
"15m",
|
||||
"30m",
|
||||
"1h",
|
||||
"2h",
|
||||
"1d"
|
||||
],
|
||||
"time_options": [
|
||||
"5m",
|
||||
"15m",
|
||||
"1h",
|
||||
"6h",
|
||||
"12h",
|
||||
"24h",
|
||||
"2d",
|
||||
"7d",
|
||||
"30d"
|
||||
]
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "test",
|
||||
"version": 1
|
||||
},
|
||||
"createdBy": "admin"
|
||||
}
|
||||
```
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **401** - Unauthorized
|
||||
- **404** - Dashboard version not found
|
||||
|
||||
## Restore dashboard
|
||||
|
||||
`POST /api/dashboards/id/:dashboardId/restore`
|
||||
|
||||
Restores a dashboard to a given dashboard version.
|
||||
|
||||
**Example request for restoring a dashboard version**:
|
||||
|
||||
```http
|
||||
POST /api/dashboards/id/1/restore
|
||||
Accept: application/json
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"version": 1
|
||||
}
|
||||
```
|
||||
|
||||
JSON body schema:
|
||||
|
||||
- **version** - The dashboard version to restore to
|
||||
|
||||
**Example response**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 67
|
||||
|
||||
{
|
||||
"slug": "my-dashboard",
|
||||
"status": "success",
|
||||
"version": 3
|
||||
}
|
||||
```
|
||||
|
||||
JSON response body schema:
|
||||
|
||||
- **slug** - the URL friendly slug of the dashboard's title
|
||||
- **status** - whether the restoration was successful or not
|
||||
- **version** - the new dashboard version, following the restoration
|
||||
|
||||
Status codes:
|
||||
|
||||
- **200** - OK
|
||||
- **401** - Unauthorized
|
||||
- **404** - Not found (dashboard not found or dashboard version not found)
|
||||
- **500** - Internal server error (indicates issue retrieving dashboard tags from database)
|
||||
|
||||
**Example error response**
|
||||
|
||||
```http
|
||||
HTTP/1.1 404 Not Found
|
||||
Content-Type: application/json; charset=UTF-8
|
||||
Content-Length: 46
|
||||
|
||||
{
|
||||
"message": "Dashboard version not found"
|
||||
}
|
||||
```
|
||||
|
||||
JSON response body schema:
|
||||
|
||||
- **message** - Message explaining the reason for the request failure.
|
||||
|
||||
## Compare dashboard versions
|
||||
|
||||
`POST /api/dashboards/calculate-diff`
|
||||
|
||||
Compares two dashboard versions by calculating the JSON diff of them.
|
||||
|
||||
**Example request**:
|
||||
|
||||
```http
|
||||
POST /api/dashboards/calculate-diff HTTP/1.1
|
||||
Accept: text/html
|
||||
Content-Type: application/json
|
||||
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
|
||||
|
||||
{
|
||||
"base": {
|
||||
"dashboardId": 1,
|
||||
"version": 1
|
||||
},
|
||||
"new": {
|
||||
"dashboardId": 1,
|
||||
"version": 2
|
||||
},
|
||||
"diffType": "json"
|
||||
}
|
||||
```
|
||||
|
||||
JSON body schema:
|
||||
|
||||
- **base** - an object representing the base dashboard version
|
||||
- **new** - an object representing the new dashboard version
|
||||
- **diffType** - the type of diff to return. Can be "json" or "basic".
|
||||
|
||||
**Example response (JSON diff)**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
<p id="l1" class="diff-line diff-json-same">
|
||||
<!-- Diff omitted -->
|
||||
</p>
|
||||
```
|
||||
|
||||
The response is a textual respresentation of the diff, with the dashboard values being in JSON, similar to the diffs seen on sites like GitHub or GitLab.
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - Ok
|
||||
- **400** - Bad request (invalid JSON sent)
|
||||
- **401** - Unauthorized
|
||||
- **404** - Not found
|
||||
|
||||
**Example response (basic diff)**:
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
<div class="diff-group">
|
||||
<!-- Diff omitted -->
|
||||
</div>
|
||||
```
|
||||
|
||||
The response here is a summary of the changes, derived from the diff between the two JSON objects.
|
||||
|
||||
Status Codes:
|
||||
|
||||
- **200** - OK
|
||||
- **400** - Bad request (invalid JSON sent)
|
||||
- **401** - Unauthorized
|
||||
- **404** - Not found
|
@ -203,6 +203,12 @@ For MySQL, use either `true`, `false`, or `skip-verify`.
|
||||
|
||||
(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`.
|
||||
|
||||
### max_idle_conn
|
||||
The maximum number of connections in the idle connection pool.
|
||||
|
||||
### max_open_conn
|
||||
The maximum number of open connections to the database.
|
||||
|
||||
<hr />
|
||||
|
||||
## [security]
|
||||
|
@ -15,7 +15,7 @@ weight = 1
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for Debian-based Linux | [grafana_4.3.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.3.1_amd64.deb)
|
||||
Stable for Debian-based Linux | [grafana_4.4.1_amd64.deb](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.1_amd64.deb)
|
||||
|
||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||
installation.
|
||||
@ -23,9 +23,9 @@ installation.
|
||||
## Install Stable
|
||||
|
||||
```bash
|
||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.3.1_amd64.deb
|
||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_4.4.1_amd64.deb
|
||||
sudo apt-get install -y adduser libfontconfig
|
||||
sudo dpkg -i grafana_4.3.1_amd64.deb
|
||||
sudo dpkg -i grafana_4.4.1_amd64.deb
|
||||
```
|
||||
|
||||
<!--
|
||||
@ -81,6 +81,7 @@ sudo apt-get install -y apt-transport-https
|
||||
- Installs systemd service (if systemd is available) name `grafana-server.service`
|
||||
- The default configuration sets the log file at `/var/log/grafana/grafana.log`
|
||||
- The default configuration specifies an sqlite3 db at `/var/lib/grafana/grafana.db`
|
||||
- Installs HTML/JS/CSS and other Grafana files at `/usr/share/grafana`
|
||||
|
||||
## Start the server (init.d service)
|
||||
|
||||
|
@ -15,7 +15,7 @@ weight = 2
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.3.1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.3.1-1.x86_64.rpm)
|
||||
Stable for CentOS / Fedora / OpenSuse / Redhat Linux | [4.4.1 (x86-64 rpm)](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.4.1-1.x86_64.rpm)
|
||||
|
||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||
installation.
|
||||
@ -24,19 +24,19 @@ installation.
|
||||
|
||||
You can install Grafana using Yum directly.
|
||||
|
||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.3.1-1.x86_64.rpm
|
||||
$ sudo yum install https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.4.1-1.x86_64.rpm
|
||||
|
||||
Or install manually using `rpm`.
|
||||
|
||||
#### On CentOS / Fedora / Redhat:
|
||||
|
||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.3.1-1.x86_64.rpm
|
||||
$ wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.4.1-1.x86_64.rpm
|
||||
$ sudo yum install initscripts fontconfig
|
||||
$ sudo rpm -Uvh grafana-4.3.1-1.x86_64.rpm
|
||||
$ sudo rpm -Uvh grafana-4.4.1-1.x86_64.rpm
|
||||
|
||||
#### On OpenSuse:
|
||||
|
||||
$ sudo rpm -i --nodeps grafana-4.3.1-1.x86_64.rpm
|
||||
$ sudo rpm -i --nodeps grafana-4.4.1-1.x86_64.rpm
|
||||
|
||||
## Install via YUM Repository
|
||||
|
||||
|
@ -13,7 +13,7 @@ weight = 3
|
||||
|
||||
Description | Download
|
||||
------------ | -------------
|
||||
Latest stable package for Windows | [grafana.4.3.1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.3.1.windows-x64.zip)
|
||||
Latest stable package for Windows | [grafana.4.4.1.windows-x64.zip](https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-4.4.1.windows-x64.zip)
|
||||
|
||||
Read [Upgrading Grafana]({{< relref "installation/upgrading.md" >}}) for tips and guidance on updating an existing
|
||||
installation.
|
||||
|
@ -15,14 +15,21 @@ dev environment. Grafana ships with its own required backend server; also comple
|
||||
|
||||
- [Go 1.8.1](https://golang.org/dl/)
|
||||
- [NodeJS LTS](https://nodejs.org/download/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
|
||||
## Get Code
|
||||
Create a directory for the project and set your path accordingly. Then download and install Grafana into your $GOPATH directory
|
||||
Create a directory for the project and set your path accordingly (or use the [default Go workspace directory](https://golang.org/doc/code.html#GOPATH)). Then download and install Grafana into your $GOPATH directory:
|
||||
|
||||
```
|
||||
export GOPATH=`pwd`
|
||||
go get github.com/grafana/grafana
|
||||
```
|
||||
|
||||
On Windows use setx instead of export and then restart your command prompt:
|
||||
```
|
||||
setx GOPATH %cd%
|
||||
```
|
||||
|
||||
You may see an error such as: `package github.com/grafana/grafana: no buildable Go source files`. This is just a warning, and you can proceed with the directions.
|
||||
|
||||
## Building the backend
|
||||
@ -36,6 +43,12 @@ go run build.go build # (or 'go build ./pkg/cmd/grafana-server')
|
||||
The Grafana backend includes Sqlite3 which requires GCC to compile. So in order to compile Grafana on windows you need
|
||||
to install GCC. We recommend [TDM-GCC](http://tdm-gcc.tdragon.net/download).
|
||||
|
||||
[node-gyp](https://github.com/nodejs/node-gyp#installation) is the Node.js native addon build tool and it requires extra dependencies to be installed on Windows. In a command prompt which is run as administrator, run:
|
||||
|
||||
```
|
||||
npm --add-python-to-path='true' --debug install --global windows-build-tools
|
||||
```
|
||||
|
||||
## Build the Front-end Assets
|
||||
|
||||
To build less to css for the frontend you will need a recent version of node (v0.12.0),
|
||||
@ -55,6 +68,8 @@ go get github.com/Unknwon/bra
|
||||
bra run
|
||||
```
|
||||
|
||||
If the `bra run` command does not work, make sure that the bin directory in your Go workspace directory is in the path. $GOPATH/bin (or %GOPATH%\bin in Windows) is in your path.
|
||||
|
||||
## Running Grafana Locally
|
||||
You can run a local instance of Grafana by running:
|
||||
```
|
||||
@ -94,3 +109,24 @@ Learn more about Grafana config options in the [Configuration section](/installa
|
||||
|
||||
## Create a pull requests
|
||||
Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Problem**: PhantomJS or node-sass errors when running grunt
|
||||
|
||||
**Solution**: delete the node_modules directory. Install [node-gyp](https://github.com/nodejs/node-gyp#installation) properly for your platform. Then run `yarn install --pure-lockfile` again.
|
||||
<br><br>
|
||||
|
||||
**Problem**: When running `bra run` for the first time you get an error that it is not a recognized command.
|
||||
|
||||
**Solution**: Add the bin directory in your Go workspace directory to the path. Per default this is `$HOME/go/bin` on Linux and `%USERPROFILE%\go\bin` on Windows or `$GOPATH/bin` (`%GOPATH%\bin` on Windows) if you have set your own workspace directory.
|
||||
<br><br>
|
||||
|
||||
**Problem**: When executing a `go get` command on Windows and you get an error about the git repository not existing.
|
||||
|
||||
**Solution**: `go get` requires Git. If you run `go get` without Git then it will create an empty directory in your Go workspace for the library you are trying to get. Even after installing Git, you will get a similar error. To fix this, delete the empty directory (for example: if you tried to run `go get github.com/Unknwon/bra` then delete `%USERPROFILE%\go\src\github.com\Unknwon\bra`) and run the `go get` command again.
|
||||
<br><br>
|
||||
|
||||
**Problem**: On Windows, getting errors about a tool not being installed even though you just installed that tool.
|
||||
|
||||
**Solution**: It is usually because it got added to the path and you have to restart your command prompt to use it.
|
||||
|
40
docs/sources/reference/dashboard_history.md
Normal file
40
docs/sources/reference/dashboard_history.md
Normal file
@ -0,0 +1,40 @@
|
||||
+++
|
||||
title = "Dashboard Version History"
|
||||
keywords = ["grafana", "dashboard", "documentation", "version", "history"]
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
name = "Dashboard Version History"
|
||||
parent = "dashboard_features"
|
||||
weight = 100
|
||||
+++
|
||||
|
||||
|
||||
# Dashboard Version History
|
||||
|
||||
Whenever you save a version of your dashboard, a copy of that version is saved so that previous versions of your dashboard are never lost. A list of these versions is available by clicking the dashboard menu dropdown, and clicking "Version history".
|
||||
|
||||
<img class="no-shadow" src="/img/docs/v4/dashboard_versions_list.png">
|
||||
|
||||
The dashboard version history feature lets you compare and restore to previously saved dashboard versions.
|
||||
|
||||
## Comparing two dashboard versions
|
||||
|
||||
To compare two dashboard versions, select the two versions from the list that you wish to compare. Once selected, the "Compare versions" button will become clickable. Click the button to view the diff between the two versions.
|
||||
|
||||
<img class="no-shadow" src="/img/docs/v4/dashboard_versions_select.png">
|
||||
|
||||
Upon clicking the button, you'll be brought to the diff view. By default, you'll see a textual summary of the changes, like in the image below.
|
||||
|
||||
<img class="no-shadow" src="/img/docs/v4/dashboard_versions_diff_basic.png">
|
||||
|
||||
If you want to view the diff of the raw JSON that represents your dashboard, you can do that as well by clicking the "JSON Diff" tab on the left.
|
||||
|
||||
If you want to restore to the version you are diffing against, you can do so by clicking the "Restore to version <x>" button in the top right.
|
||||
|
||||
## Restoring to a previously saved dashboard version
|
||||
|
||||
If you need to restore to a previously saved dashboard version, you can do so by either clicking the "Restore" button on the right of a row in the dashboard version list, or by clicking the "Restore to version <x>" button appearing in the diff view. Clicking the button will bring up the following popup prompting you to confirm the restoration.
|
||||
|
||||
<img class="no-shadow" src="/img/docs/v4/dashboard_versions_restore.png">
|
||||
|
||||
After restoring to a previous version, a new version will be created containing the same exact data as the previous version, only with a different version number. This is indicated in the "Notes column" for the row in the new dashboard version. This is done simply to ensure your previous dashboard versions are not affected by the change.
|
@ -8,10 +8,12 @@ weight = 6
|
||||
+++
|
||||
|
||||
# Sharing features
|
||||
|
||||
Grafana provides a number of ways to share a dashboard or a specific panel to other users within your
|
||||
organization. It also provides ways to publish interactive snapshots that can be accessed by external partners.
|
||||
|
||||
## Share dashboard
|
||||
|
||||
Share a dashboard via the share icon in the top nav. This opens the share dialog where you
|
||||
can get a link to the current dashboard with the current selected time range and template variables. If you have
|
||||
made changes to the dashboard, make sure those are saved before sending the link.
|
||||
@ -25,18 +27,35 @@ snapshots can be accessed by anyone who has the link and can reach the URL.
|
||||

|
||||
|
||||
### Publish snapshots
|
||||
|
||||
You can publish snapshots to you local instance or to [snapshot.raintank.io](http://snapshot.raintank.io). The later is a free service
|
||||
that is provided by [Raintank](http://raintank.io) that allows you to publish dashboard snapshots to an external grafana instance.
|
||||
The same rules still apply, anyone with the link can view it. You can set an expiration time if you want the snapshot to be removed
|
||||
after a certain time period.
|
||||
|
||||
## Share Panel
|
||||
Click a panel title to open the panel menu, then click share in the panel menu to open the Share Panel dialog. Here you
|
||||
have access to a link that will take you to exactly this panel with the current time range and selected template variables.
|
||||
You also get a link to service side rendered PNG of the panel. Useful if you want to share an image of the panel.
|
||||
Please note that for OSX and Windows, you will need to ensure that a `phantomjs` binary is available under `vendor/phantomjs/phantomjs`. For Linux, a `phantomjs` binary is included - however, you should ensure that any requisite libraries (e.g. libfontconfig) are available.
|
||||
|
||||
Click a panel title to open the panel menu, then click share in the panel menu to open the Share Panel dialog. Here you have access to a link that will take you to exactly this panel with the current time range and selected template variables.
|
||||
|
||||
### Direct Link Rendered Image
|
||||
|
||||
You also get a link to service side rendered PNG of the panel. Useful if you want to share an image of the panel. Please note that for OSX and Windows, you will need to ensure that a `phantomjs` binary is available under `vendor/phantomjs/phantomjs`. For Linux, a `phantomjs` binary is included - however, you should ensure that any requisite libraries (e.g. libfontconfig) are available.
|
||||
|
||||
Example of a link to a server-side rendered PNG:
|
||||
|
||||
```
|
||||
http://play.grafana.org/render/dashboard-solo/db/grafana-play-home?orgId=1&panelId=4&from=1499272191563&to=1499279391563&width=1000&height=500&tz=UTC%2B02%3A00&timeout=5000
|
||||
```
|
||||
|
||||
#### Query String Parameters For Server-Side Rendered Images
|
||||
|
||||
- **width**: width in pixels. Default is 800.
|
||||
- **height**: height in pixels. Default is 400.
|
||||
- **tz**: timezone in the format `UTC%2BHH%3AMM` where HH and MM are offset in hours and minutes after UTC
|
||||
- **timeout**: number of seconds. The timeout can be increased if the query for the panel needs more than the default 30 seconds.
|
||||
|
||||
### Embed Panel
|
||||
|
||||
You can embed a panel using an iframe on another web site. This tab will show you the html that you need to use.
|
||||
|
||||
Example:
|
||||
@ -47,3 +66,14 @@ Example:
|
||||
|
||||
Below there should be an interactive Grafana graph embedded in an iframe:
|
||||
<iframe src="https://snapshot.raintank.io/dashboard-solo/snapshot/y7zwi2bZ7FcoTlB93WN7yWO4aMiz3pZb?from=1493369923321&to=1493377123321&panelId=4" width="650" height="300" frameborder="0"></iframe>
|
||||
|
||||
### Export Panel Data
|
||||
|
||||

|
||||
|
||||
The submenu for a panel can be found by clicking on the title of a panel and then on the hamburger (three horizontal lines) submenu on the left of the context menu.
|
||||
|
||||
This menu contains two options for exporting data:
|
||||
|
||||
- The panel JSON (the specification and not the data) can be exported or updated via the panel context menu.
|
||||
- Panel data can be exported in the CSV format for Table and Graph Panels.
|
||||
|
@ -141,6 +141,42 @@ Use the `Interval` type to create a variable that represents a time span (eg. `1
|
||||
|
||||
This variable type is useful as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
|
||||
|
||||
Example using the template variable `myinterval` of type `Interval` in a graphite function:
|
||||
|
||||
```
|
||||
summarize($myinterval, sum, false)
|
||||
```
|
||||
|
||||
## Global Built-in Variables
|
||||
|
||||
Grafana has global built-in variables that can be used in expressions in the query editor.
|
||||
|
||||
### The $__interval Variable
|
||||
|
||||
This $__interval variable is similar to the `auto` interval variable that is described above. It can be used as a parameter to group by time (for InfluxDB), Date histogram interval (for Elasticsearch) or as a *summarize* function parameter (for Graphite).
|
||||
|
||||
Grafana automatically calculates an interval that can be used to group by time in queries. When there are more data points than can be shown on a graph then queries can be made more efficient by grouping by a larger interval. It is more efficient to group by 1 day than by 10s when looking at 3 months of data and the graph will look the same and the query will be faster. The `$__interval` is calculated using the time range and the width of the graph (the number of pixels).
|
||||
|
||||
Approximate Calculation: `(from - to) / resolution`
|
||||
|
||||
For example, when the time range is 1 hour and the graph is full screen, then the interval might be calculated to `2m` - points are grouped in 2 minute intervals. If the time range is 6 months and the graph is full screen, then the interval might be `1d` (1 day) - points are grouped by day.
|
||||
|
||||
In the InfluxDB data source, the legacy variable `$interval` is the same variable. `$__interval` should be used instead.
|
||||
|
||||
The InfluxDB and Elasticsearch data sources have `Group by time interval` fields that are used to hard code the interval or to set the minimum limit for the `$__interval` variable (by using the `>` syntax -> `>10m`).
|
||||
|
||||
### The $__interval_ms Variable
|
||||
|
||||
This variable is the `$__interval` variable in milliseconds (and not a time interval formatted string). For example, if the `$__interval` is `20m` then the `$__interval_ms` is `1200000`.
|
||||
|
||||
### The $timeFilter or $__timeFilter Variable
|
||||
|
||||
The `$timeFilter` variable returns the currently selected time range as an expression. For example, the time range interval `Last 7 days` expression is `time > now() - 7d`.
|
||||
|
||||
This is used in the WHERE clause for the InfluxDB data source. Grafana adds it automatically to InfluxDB queries when in Query Editor Mode. It has to be added manually in Text Editor Mode: `WHERE $timeFilter`.
|
||||
|
||||
The `$__timeFilter` is used in the MySQL data source.
|
||||
|
||||
## Repeating Panels
|
||||
|
||||
Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want
|
||||
|
@ -35,6 +35,4 @@ But we suggest that you store the session in redis/memcache since it makes it ea
|
||||
|
||||
## Alerting
|
||||
|
||||
Currently alerting does not support high availability. But this is something that we will be working on in the future.
|
||||
|
||||
|
||||
Currently alerting supports a limited form of high availability. Since v4.2.0 of Grafana, alert notifications are deduped when running multiple servers. This means all alerts are executed on every server but no duplicate alert notifications are sent due to the deduping logic. Proper load balancing of alerts will be introduced in the future.
|
||||
|
@ -4,7 +4,7 @@
|
||||
"company": "Coding Instinct AB"
|
||||
},
|
||||
"name": "grafana",
|
||||
"version": "4.4.0-pre1",
|
||||
"version": "5.0.0-pre1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "http://github.com/grafana/grafana.git"
|
||||
|
@ -1,5 +1,5 @@
|
||||
#! /usr/bin/env bash
|
||||
version=4.3.1
|
||||
version=4.4.1
|
||||
|
||||
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${version}_amd64.deb
|
||||
|
||||
|
@ -285,6 +285,7 @@ func (hs *HttpServer) registerRoutes() {
|
||||
}, reqEditorRole)
|
||||
|
||||
r.Get("/annotations", wrap(GetAnnotations))
|
||||
r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
|
||||
|
||||
r.Group("/annotations", func() {
|
||||
r.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
|
||||
|
@ -171,6 +171,8 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
|
||||
m.Use(middleware.ValidateHostHeader(setting.Domain))
|
||||
}
|
||||
|
||||
m.Use(middleware.AddDefaultResponseHeaders())
|
||||
|
||||
return m
|
||||
}
|
||||
|
||||
|
@ -78,6 +78,11 @@ func AddOrgInvite(c *middleware.Context, inviteDto dtos.AddInviteForm) Response
|
||||
return ApiError(500, "Failed to send email invite", err)
|
||||
}
|
||||
|
||||
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
|
||||
if err := bus.Dispatch(&emailSentCmd); err != nil {
|
||||
return ApiError(500, "Failed to update invite with email sent info", err)
|
||||
}
|
||||
|
||||
return ApiSuccess(fmt.Sprintf("Sent invite to %s", inviteDto.LoginOrEmail))
|
||||
}
|
||||
|
||||
|
@ -42,6 +42,7 @@ func GetUserByLoginOrEmail(c *middleware.Context) Response {
|
||||
}
|
||||
user := query.Result
|
||||
result := m.UserProfileDTO{
|
||||
Id: user.Id,
|
||||
Name: user.Name,
|
||||
Email: user.Email,
|
||||
Login: user.Login,
|
||||
|
@ -71,6 +71,8 @@ func NewBasicFormatter(left interface{}) *BasicFormatter {
|
||||
}
|
||||
}
|
||||
|
||||
// Format takes the diff of two JSON documents, and returns the difference
|
||||
// between them summarized in an HTML document.
|
||||
func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
|
||||
// calling jsonDiff.Format(d) populates the JSON diff's "Lines" value,
|
||||
// which we use to compute the basic dif
|
||||
@ -90,67 +92,45 @@ func (b *BasicFormatter) Format(d diff.Diff) ([]byte, error) {
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
// Basic is V2 of the basic diff
|
||||
// Basic transforms a slice of JSONLines into a slice of BasicBlocks.
|
||||
func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
|
||||
// init an array you can append to for the basic "blocks"
|
||||
blocks := make([]*BasicBlock, 0)
|
||||
|
||||
// iterate through each line
|
||||
for _, line := range lines {
|
||||
// TODO: this condition needs an explaination? what does it mean?
|
||||
if b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil {
|
||||
if b.returnToTopLevelKey(line) {
|
||||
if b.Block != nil {
|
||||
blocks = append(blocks, b.Block)
|
||||
}
|
||||
}
|
||||
|
||||
// Record the last indent level at each pass in case we need to
|
||||
// check for a change in depth inside the JSON data structures.
|
||||
b.LastIndent = line.Indent
|
||||
|
||||
// TODO: why special handling for indent 2?
|
||||
if line.Indent == 1 {
|
||||
switch line.Change {
|
||||
case ChangeNil:
|
||||
if line.Change == ChangeNil {
|
||||
if line.Key != "" {
|
||||
b.Block = &BasicBlock{
|
||||
Title: line.Key,
|
||||
Change: line.Change,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ChangeAdded, ChangeDeleted:
|
||||
blocks = append(blocks, &BasicBlock{
|
||||
Title: line.Key,
|
||||
Change: line.Change,
|
||||
New: line.Val,
|
||||
LineStart: line.LineNum,
|
||||
})
|
||||
|
||||
case ChangeOld:
|
||||
b.Block = &BasicBlock{
|
||||
Title: line.Key,
|
||||
Old: line.Val,
|
||||
Change: line.Change,
|
||||
LineStart: line.LineNum,
|
||||
}
|
||||
|
||||
case ChangeNew:
|
||||
b.Block.New = line.Val
|
||||
b.Block.LineEnd = line.LineNum
|
||||
|
||||
// then write out the change
|
||||
blocks = append(blocks, b.Block)
|
||||
default:
|
||||
// ok
|
||||
if block, ok := b.handleTopLevelChange(line); ok {
|
||||
blocks = append(blocks, block)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: why special handling for indent > 2 ?
|
||||
// Other Lines
|
||||
// Here is where we handle changes for all types, appending each change
|
||||
// to the current block based on the value.
|
||||
//
|
||||
// Values which only occupy a single line in JSON (like a string or
|
||||
// int, for example) are treated as "Basic Changes" that we append to
|
||||
// the current block as soon as they're detected.
|
||||
//
|
||||
// Values which occupy multiple lines (either slices or maps) are
|
||||
// treated as "Basic Summaries". When we detect the "ChangeNil" type,
|
||||
// we know we've encountered one of these types, so we record the
|
||||
// starting position as well the type of the change, and stop
|
||||
// performing comparisons until we find the end of that change. Upon
|
||||
// finding the change, we append it to the current block, and begin
|
||||
// performing comparisons again.
|
||||
if line.Indent > 1 {
|
||||
// Ensure single line change
|
||||
if line.Key != "" && line.Val != nil && !b.writing {
|
||||
// check to ensure a single line change
|
||||
if b.isSingleLineChange(line) {
|
||||
switch line.Change {
|
||||
case ChangeAdded, ChangeDeleted:
|
||||
|
||||
@ -178,13 +158,31 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
|
||||
//ok
|
||||
}
|
||||
|
||||
// otherwise, we're dealing with a change at a deeper level. We
|
||||
// know there's a change somewhere in the JSON tree, but we
|
||||
// don't know exactly where, so we go deeper.
|
||||
} else {
|
||||
|
||||
// if the change is anything but unchanged, continue processing
|
||||
//
|
||||
// we keep "narrowing" the key as we go deeper, in order to
|
||||
// correctly report the key name for changes found within an
|
||||
// object or array.
|
||||
if line.Change != ChangeUnchanged {
|
||||
if line.Key != "" {
|
||||
b.narrow = line.Key
|
||||
b.keysIdent = line.Indent
|
||||
}
|
||||
|
||||
// if the change isn't nil, and we're not already writing
|
||||
// out a change, then we've found something.
|
||||
//
|
||||
// First, try to determine the title of the embedded JSON
|
||||
// object. If it's an empty string, then we're in an object
|
||||
// or array, so we default to using the "narrowed" key.
|
||||
//
|
||||
// We also start recording the basic summary, until we find
|
||||
// the next `ChangeUnchanged`.
|
||||
if line.Change != ChangeNil {
|
||||
if !b.writing {
|
||||
b.writing = true
|
||||
@ -204,6 +202,17 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
|
||||
}
|
||||
}
|
||||
}
|
||||
// if we find a `ChangeUnchanged`, we do one of two things:
|
||||
//
|
||||
// - if we're recording a change already, then we know
|
||||
// we've come to the end of that change block, so we write
|
||||
// that change out be recording the line number of where
|
||||
// that change ends, and append it to the current block's
|
||||
// summary.
|
||||
//
|
||||
// - if we're not recording a change, then we do nothing,
|
||||
// since the BasicDiff doesn't report on unchanged JSON
|
||||
// values.
|
||||
} else {
|
||||
if b.writing {
|
||||
b.writing = false
|
||||
@ -218,6 +227,81 @@ func (b *BasicDiff) Basic(lines []*JSONLine) []*BasicBlock {
|
||||
return blocks
|
||||
}
|
||||
|
||||
// returnToTopLevelKey indicates that we've moved from a key at one level deep
|
||||
// in the JSON document to a top level key.
|
||||
//
|
||||
// In order to produce distinct "blocks" when rendering the basic diff,
|
||||
// we need a way to distinguish between differnt sections of data.
|
||||
// To do this, we consider the value(s) of each top-level JSON key to
|
||||
// represent a distinct block for Grafana's JSON data structure, so
|
||||
// we perform this check to see if we've entered a new "block". If we
|
||||
// have, we simply append the existing block to the array of blocks.
|
||||
func (b *BasicDiff) returnToTopLevelKey(line *JSONLine) bool {
|
||||
return b.LastIndent == 2 && line.Indent == 1 && line.Change == ChangeNil
|
||||
}
|
||||
|
||||
// handleTopLevelChange handles a change on one of the top-level keys on a JSON
|
||||
// document.
|
||||
//
|
||||
// If the line's indentation is at level 1, then we know it's a top
|
||||
// level key in the JSON document. As mentioned earlier, we treat these
|
||||
// specially as they indicate their values belong to distinct blocks.
|
||||
//
|
||||
// At level 1, we only record single-line changes, ie, the "added",
|
||||
// "deleted", "old" or "new" cases, since we know those values aren't
|
||||
// arrays or maps. We only handle these cases at level 2 or deeper,
|
||||
// since for those we either output a "change" or "summary". This is
|
||||
// done for formatting reasons only, so we have logical "blocks" to
|
||||
// display.
|
||||
func (b *BasicDiff) handleTopLevelChange(line *JSONLine) (*BasicBlock, bool) {
|
||||
switch line.Change {
|
||||
case ChangeNil:
|
||||
if line.Change == ChangeNil {
|
||||
if line.Key != "" {
|
||||
b.Block = &BasicBlock{
|
||||
Title: line.Key,
|
||||
Change: line.Change,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case ChangeAdded, ChangeDeleted:
|
||||
return &BasicBlock{
|
||||
Title: line.Key,
|
||||
Change: line.Change,
|
||||
New: line.Val,
|
||||
LineStart: line.LineNum,
|
||||
}, true
|
||||
|
||||
case ChangeOld:
|
||||
b.Block = &BasicBlock{
|
||||
Title: line.Key,
|
||||
Old: line.Val,
|
||||
Change: line.Change,
|
||||
LineStart: line.LineNum,
|
||||
}
|
||||
|
||||
case ChangeNew:
|
||||
b.Block.New = line.Val
|
||||
b.Block.LineEnd = line.LineNum
|
||||
|
||||
// For every "old" change there is a corresponding "new", which
|
||||
// is why we wait until we detect the "new" change before
|
||||
// appending the change.
|
||||
return b.Block, true
|
||||
default:
|
||||
// ok
|
||||
}
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// isSingleLineChange ensures we're iterating over a single line change (ie,
|
||||
// either a single line or a old-new value pair was changed in the JSON file).
|
||||
func (b *BasicDiff) isSingleLineChange(line *JSONLine) bool {
|
||||
return line.Key != "" && line.Val != nil && !b.writing
|
||||
}
|
||||
|
||||
// encStateMap is used in the template helper
|
||||
var (
|
||||
encStateMap = map[ChangeType]string{
|
||||
@ -240,7 +324,9 @@ var (
|
||||
)
|
||||
|
||||
var (
|
||||
// tplBlock is the whole thing
|
||||
// tplBlock is the container for the basic diff. It iterates over each
|
||||
// basic block, expanding each "change" and "summary" belonging to every
|
||||
// block.
|
||||
tplBlock = `{{ define "block" -}}
|
||||
{{ range . }}
|
||||
<div class="diff-group">
|
||||
@ -286,7 +372,7 @@ var (
|
||||
{{ end }}
|
||||
{{ end }}`
|
||||
|
||||
// tplChange is the template for changes
|
||||
// tplChange is the template for basic changes.
|
||||
tplChange = `{{ define "change" -}}
|
||||
<li class="diff-change-group">
|
||||
<span class="bullet-position-container">
|
||||
@ -313,7 +399,7 @@ var (
|
||||
</li>
|
||||
{{ end }}`
|
||||
|
||||
// tplSummary is for basis summaries
|
||||
// tplSummary is for basic summaries.
|
||||
tplSummary = `{{ define "summary" -}}
|
||||
<div class="diff-group-name">
|
||||
<i class="diff-circle diff-circle-{{ getChange .Change }} fa fa-circle-o diff-list-circle"></i>
|
||||
|
131
pkg/components/dashdiffs/formatter_test.go
Normal file
131
pkg/components/dashdiffs/formatter_test.go
Normal file
@ -0,0 +1,131 @@
|
||||
package dashdiffs
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDiff(t *testing.T) {
|
||||
// Sample json docs for tests only
|
||||
const (
|
||||
leftJSON = `{
|
||||
"key": "value",
|
||||
"object": {
|
||||
"key": "value",
|
||||
"anotherObject": {
|
||||
"same": "this field is the same in rightJSON",
|
||||
"change": "this field should change in rightJSON",
|
||||
"delete": "this field doesn't appear in rightJSON"
|
||||
}
|
||||
},
|
||||
"array": [
|
||||
"same",
|
||||
"change",
|
||||
"delete"
|
||||
],
|
||||
"embeddedArray": {
|
||||
"array": [
|
||||
"same",
|
||||
"change",
|
||||
"delete"
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
rightJSON = `{
|
||||
"key": "differentValue",
|
||||
"object": {
|
||||
"key": "value",
|
||||
"newKey": "value",
|
||||
"anotherObject": {
|
||||
"same": "this field is the same in rightJSON",
|
||||
"change": "this field should change in rightJSON",
|
||||
"add": "this field is added"
|
||||
}
|
||||
},
|
||||
"array": [
|
||||
"same",
|
||||
"changed!",
|
||||
"add"
|
||||
],
|
||||
"embeddedArray": {
|
||||
"array": [
|
||||
"same",
|
||||
"changed!",
|
||||
"add"
|
||||
]
|
||||
}
|
||||
}`
|
||||
)
|
||||
|
||||
Convey("Testing dashboard diffs", t, func() {
|
||||
|
||||
// Compute the diff between the two JSON objects
|
||||
baseData, err := simplejson.NewJson([]byte(leftJSON))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
newData, err := simplejson.NewJson([]byte(rightJSON))
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
left, jsonDiff, err := getDiff(baseData, newData)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("The JSONFormatter should produce the expected JSON tokens", func() {
|
||||
f := NewJSONFormatter(left)
|
||||
_, err := f.Format(jsonDiff)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
// Total up the change types. If the number of different change
|
||||
// types is correct, it means that the diff is producing correct
|
||||
// output to the template rendered.
|
||||
changeCounts := make(map[ChangeType]int)
|
||||
for _, line := range f.Lines {
|
||||
changeCounts[line.Change]++
|
||||
}
|
||||
|
||||
// The expectedChangeCounts here were determined by manually
|
||||
// looking at the JSON
|
||||
expectedChangeCounts := map[ChangeType]int{
|
||||
ChangeNil: 12,
|
||||
ChangeAdded: 2,
|
||||
ChangeDeleted: 1,
|
||||
ChangeOld: 5,
|
||||
ChangeNew: 5,
|
||||
ChangeUnchanged: 5,
|
||||
}
|
||||
So(changeCounts, ShouldResemble, expectedChangeCounts)
|
||||
})
|
||||
|
||||
Convey("The BasicFormatter should produce the expected BasicBlocks", func() {
|
||||
f := NewBasicFormatter(left)
|
||||
_, err := f.Format(jsonDiff)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
bd := &BasicDiff{}
|
||||
blocks := bd.Basic(f.jsonDiff.Lines)
|
||||
|
||||
changeCounts := make(map[ChangeType]int)
|
||||
for _, block := range blocks {
|
||||
for _, change := range block.Changes {
|
||||
changeCounts[change.Change]++
|
||||
}
|
||||
|
||||
for _, summary := range block.Summaries {
|
||||
changeCounts[summary.Change]++
|
||||
}
|
||||
|
||||
changeCounts[block.Change]++
|
||||
}
|
||||
|
||||
expectedChangeCounts := map[ChangeType]int{
|
||||
ChangeNil: 3,
|
||||
ChangeAdded: 2,
|
||||
ChangeDeleted: 1,
|
||||
ChangeOld: 3,
|
||||
}
|
||||
So(changeCounts, ShouldResemble, expectedChangeCounts)
|
||||
})
|
||||
})
|
||||
}
|
@ -44,6 +44,7 @@ var (
|
||||
M_Alerting_Notification_Sent_Slack Counter
|
||||
M_Alerting_Notification_Sent_Email Counter
|
||||
M_Alerting_Notification_Sent_Webhook Counter
|
||||
M_Alerting_Notification_Sent_DingDing Counter
|
||||
M_Alerting_Notification_Sent_PagerDuty Counter
|
||||
M_Alerting_Notification_Sent_LINE Counter
|
||||
M_Alerting_Notification_Sent_Victorops Counter
|
||||
@ -116,6 +117,7 @@ func initMetricVars(settings *MetricSettings) {
|
||||
M_Alerting_Notification_Sent_Slack = RegCounter("alerting.notifications_sent", "type", "slack")
|
||||
M_Alerting_Notification_Sent_Email = RegCounter("alerting.notifications_sent", "type", "email")
|
||||
M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook")
|
||||
M_Alerting_Notification_Sent_DingDing = RegCounter("alerting.notifications_sent", "type", "dingding")
|
||||
M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty")
|
||||
M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops")
|
||||
M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie")
|
||||
|
@ -245,3 +245,13 @@ func (ctx *Context) HasHelpFlag(flag m.HelpFlags1) bool {
|
||||
func (ctx *Context) TimeRequest(timer metrics.Timer) {
|
||||
ctx.Data["perfmon.timer"] = timer
|
||||
}
|
||||
|
||||
func AddDefaultResponseHeaders() macaron.Handler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.IsApiRequest() && ctx.Req.Method == "GET" {
|
||||
ctx.Resp.Header().Add("Cache-Control", "no-cache")
|
||||
ctx.Resp.Header().Add("Pragma", "no-cache")
|
||||
ctx.Resp.Header().Add("Expires", "-1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,6 +30,18 @@ func TestMiddlewareContext(t *testing.T) {
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
middlewareScenario("middleware should add Cache-Control header for GET requests to API", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/api/search").exec()
|
||||
So(sc.resp.Header().Get("Cache-Control"), ShouldEqual, "no-cache")
|
||||
So(sc.resp.Header().Get("Pragma"), ShouldEqual, "no-cache")
|
||||
So(sc.resp.Header().Get("Expires"), ShouldEqual, "-1")
|
||||
})
|
||||
|
||||
middlewareScenario("middleware should not add Cache-Control header to for non-API GET requests", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").exec()
|
||||
So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
|
||||
})
|
||||
|
||||
middlewareScenario("Non api request should init session", func(sc *scenarioContext) {
|
||||
sc.fakeReq("GET", "/").exec()
|
||||
So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess")
|
||||
@ -327,6 +339,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
|
||||
startSessionGC = func() {}
|
||||
sc.m.Use(Sessioner(&session.Options{}))
|
||||
sc.m.Use(OrgRedirect())
|
||||
sc.m.Use(AddDefaultResponseHeaders())
|
||||
|
||||
sc.defaultHandler = func(c *Context) {
|
||||
sc.context = c
|
||||
|
@ -60,6 +60,10 @@ type UpdateTempUserStatusCommand struct {
|
||||
Status TempUserStatus
|
||||
}
|
||||
|
||||
type UpdateTempUserWithEmailSentCommand struct {
|
||||
Code string
|
||||
}
|
||||
|
||||
type GetTempUsersQuery struct {
|
||||
OrgId int64
|
||||
Email string
|
||||
|
@ -163,6 +163,7 @@ type SignedInUser struct {
|
||||
}
|
||||
|
||||
type UserProfileDTO struct {
|
||||
Id int64 `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
Login string `json:"login"`
|
||||
|
90
pkg/services/alerting/notifiers/dingding.go
Normal file
90
pkg/services/alerting/notifiers/dingding.go
Normal file
@ -0,0 +1,90 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func init() {
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "dingding",
|
||||
Name: "DingDing",
|
||||
Description: "Sends HTTP POST request to DingDing",
|
||||
Factory: NewDingDingNotifier,
|
||||
OptionsTemplate: `
|
||||
<h3 class="page-heading">DingDing settings</h3>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Url</span>
|
||||
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
|
||||
</div>
|
||||
`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
|
||||
}
|
||||
|
||||
return &DingDingNotifier{
|
||||
NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.dingding"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
type DingDingNotifier struct {
|
||||
NotifierBase
|
||||
Url string
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
this.log.Info("Sending dingding")
|
||||
metrics.M_Alerting_Notification_Sent_DingDing.Inc(1)
|
||||
|
||||
messageUrl, err := evalContext.GetRuleUrl()
|
||||
if err != nil {
|
||||
this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name)
|
||||
messageUrl = ""
|
||||
}
|
||||
this.log.Info("messageUrl:" + messageUrl)
|
||||
|
||||
message := evalContext.Rule.Message
|
||||
picUrl := evalContext.ImagePublicUrl
|
||||
title := evalContext.GetNotificationTitle()
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(`{
|
||||
"msgtype": "link",
|
||||
"link": {
|
||||
"text": "` + message + `",
|
||||
"title": "` + title + `",
|
||||
"picUrl": "` + picUrl + `",
|
||||
"messageUrl": "` + messageUrl + `"
|
||||
}
|
||||
}`))
|
||||
|
||||
if err != nil {
|
||||
this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)
|
||||
}
|
||||
|
||||
body, _ := bodyJSON.MarshalJSON()
|
||||
|
||||
cmd := &m.SendWebhookSync{
|
||||
Url: this.Url,
|
||||
Body: string(body),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
|
||||
this.log.Error("Failed to send DingDing", "error", err, "dingding", this.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
49
pkg/services/alerting/notifiers/dingding_test.go
Normal file
49
pkg/services/alerting/notifiers/dingding_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestDingDingNotifier(t *testing.T) {
|
||||
Convey("Line notifier tests", t, func() {
|
||||
Convey("empty settings should return error", func() {
|
||||
json := `{ }`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "dingding_testing",
|
||||
Type: "dingding",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewDingDingNotifier(model)
|
||||
So(err, ShouldNotBeNil)
|
||||
|
||||
})
|
||||
Convey("settings should trigger incident", func() {
|
||||
json := `
|
||||
{
|
||||
"url": "https://www.google.com"
|
||||
}`
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
model := &m.AlertNotification{
|
||||
Name: "dingding_testing",
|
||||
Type: "dingding",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
not, err := NewDingDingNotifier(model)
|
||||
notifier := not.(*DingDingNotifier)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(notifier.Name, ShouldEqual, "dingding_testing")
|
||||
So(notifier.Type, ShouldEqual, "dingding")
|
||||
So(notifier.Url, ShouldEqual, "https://www.google.com")
|
||||
})
|
||||
|
||||
})
|
||||
}
|
@ -146,7 +146,7 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
return sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
err := sendEmailCommandHandler(&m.SendEmailCommand{
|
||||
To: []string{evt.Email},
|
||||
Template: tmplSignUpStarted,
|
||||
Data: map[string]interface{}{
|
||||
@ -155,6 +155,12 @@ func signUpStartedHandler(evt *events.SignUpStarted) error {
|
||||
"SignUpUrl": setting.ToAbsUrl(fmt.Sprintf("signup/?email=%s&code=%s", url.QueryEscape(evt.Email), url.QueryEscape(evt.Code))),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
emailSentCmd := m.UpdateTempUserWithEmailSentCommand{Code: evt.Code}
|
||||
return bus.Dispatch(&emailSentCmd)
|
||||
}
|
||||
|
||||
func signUpCompletedHandler(evt *events.SignUpCompleted) error {
|
||||
|
@ -16,6 +16,8 @@ func InitTestDB(t *testing.T) {
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Mysql.DriverName, sqlutil.TestDB_Mysql.ConnStr)
|
||||
//x, err := xorm.NewEngine(sqlutil.TestDB_Postgres.DriverName, sqlutil.TestDB_Postgres.ConnStr)
|
||||
|
||||
// x.ShowSQL()
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to init in memory sqllite3 db %v", err)
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ SELECT
|
||||
dashboard.version,
|
||||
dashboard.version,
|
||||
dashboard.updated,
|
||||
dashboard.updated_by,
|
||||
COALESCE(dashboard.updated_by, -1),
|
||||
'',
|
||||
dashboard.data
|
||||
FROM dashboard;`
|
||||
|
@ -12,6 +12,7 @@ func init() {
|
||||
bus.AddHandler("sql", GetTempUsersQuery)
|
||||
bus.AddHandler("sql", UpdateTempUserStatus)
|
||||
bus.AddHandler("sql", GetTempUserByCode)
|
||||
bus.AddHandler("sql", UpdateTempUserWithEmailSent)
|
||||
}
|
||||
|
||||
func UpdateTempUserStatus(cmd *m.UpdateTempUserStatusCommand) error {
|
||||
@ -35,6 +36,7 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error {
|
||||
Status: cmd.Status,
|
||||
RemoteAddr: cmd.RemoteAddr,
|
||||
InvitedByUserId: cmd.InvitedByUserId,
|
||||
EmailSentOn: time.Now(),
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
}
|
||||
@ -48,6 +50,19 @@ func CreateTempUser(cmd *m.CreateTempUserCommand) error {
|
||||
})
|
||||
}
|
||||
|
||||
func UpdateTempUserWithEmailSent(cmd *m.UpdateTempUserWithEmailSentCommand) error {
|
||||
return inTransaction(func(sess *DBSession) error {
|
||||
user := &m.TempUser{
|
||||
EmailSent: true,
|
||||
EmailSentOn: time.Now(),
|
||||
}
|
||||
|
||||
_, err := sess.Where("code = ?", cmd.Code).Cols("email_sent", "email_sent_on").Update(user)
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func GetTempUsersQuery(query *m.GetTempUsersQuery) error {
|
||||
rawSql := `SELECT
|
||||
tu.id as id,
|
||||
|
@ -54,6 +54,19 @@ func TestTempUserCommandsAndQueries(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
})
|
||||
|
||||
Convey("Should be able update email sent and email sent on", func() {
|
||||
cmd3 := m.UpdateTempUserWithEmailSentCommand{Code: cmd.Result.Code}
|
||||
err := UpdateTempUserWithEmailSent(&cmd3)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
query := m.GetTempUsersQuery{OrgId: 2256, Status: m.TmpUserInvitePending}
|
||||
err = GetTempUsersQuery(&query)
|
||||
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result[0].EmailSent, ShouldBeTrue)
|
||||
So(query.Result[0].EmailSentOn, ShouldHappenOnOrAfter, (query.Result[0].Created))
|
||||
})
|
||||
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package setting
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
@ -52,13 +53,22 @@ func TestLoadingSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Should be able to override via command line", func() {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"},
|
||||
})
|
||||
if runtime.GOOS == "windows" {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`, `cfg:paths.logs=c:\tmp\logs`},
|
||||
})
|
||||
So(DataPath, ShouldEqual, `c:\tmp\data`)
|
||||
So(LogsPath, ShouldEqual, `c:\tmp\logs`)
|
||||
} else {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=/tmp/data", "cfg:paths.logs=/tmp/logs"},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/data")
|
||||
So(LogsPath, ShouldEqual, "/tmp/logs")
|
||||
So(DataPath, ShouldEqual, "/tmp/data")
|
||||
So(LogsPath, ShouldEqual, "/tmp/logs")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Should be able to override defaults via command line", func() {
|
||||
@ -74,33 +84,63 @@ func TestLoadingSettings(t *testing.T) {
|
||||
})
|
||||
|
||||
Convey("Defaults can be overridden in specified config file", func() {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
|
||||
Args: []string{"cfg:default.paths.data=/tmp/data"},
|
||||
})
|
||||
if runtime.GOOS == "windows" {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"),
|
||||
Args: []string{`cfg:default.paths.data=c:\tmp\data`},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/override")
|
||||
So(DataPath, ShouldEqual, `c:\tmp\override`)
|
||||
} else {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
|
||||
Args: []string{"cfg:default.paths.data=/tmp/data"},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/override")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Command line overrides specified config file", func() {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
|
||||
Args: []string{"cfg:paths.data=/tmp/data"},
|
||||
})
|
||||
if runtime.GOOS == "windows" {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override_windows.ini"),
|
||||
Args: []string{`cfg:paths.data=c:\tmp\data`},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/data")
|
||||
So(DataPath, ShouldEqual, `c:\tmp\data`)
|
||||
} else {
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Config: filepath.Join(HomePath, "tests/config-files/override.ini"),
|
||||
Args: []string{"cfg:paths.data=/tmp/data"},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/data")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("Can use environment variables in config values", func() {
|
||||
os.Setenv("GF_DATA_PATH", "/tmp/env_override")
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
if runtime.GOOS == "windows" {
|
||||
os.Setenv("GF_DATA_PATH", `c:\tmp\env_override`)
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/env_override")
|
||||
So(DataPath, ShouldEqual, `c:\tmp\env_override`)
|
||||
} else {
|
||||
os.Setenv("GF_DATA_PATH", "/tmp/env_override")
|
||||
NewConfigContext(&CommandLineArgs{
|
||||
HomePath: "../../",
|
||||
Args: []string{"cfg:paths.data=${GF_DATA_PATH}"},
|
||||
})
|
||||
|
||||
So(DataPath, ShouldEqual, "/tmp/env_override")
|
||||
}
|
||||
})
|
||||
|
||||
Convey("instance_name default to hostname even if hostname env is empty", func() {
|
||||
|
@ -183,6 +183,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows)
|
||||
values := make([]interface{}, len(types))
|
||||
|
||||
for i, stype := range types {
|
||||
e.log.Info("type", "type", stype)
|
||||
switch stype.DatabaseTypeName() {
|
||||
case mysql.FieldTypeNameTiny:
|
||||
values[i] = new(int8)
|
||||
@ -209,7 +210,7 @@ func (e MysqlExecutor) getTypedRowData(types []*sql.ColumnType, rows *core.Rows)
|
||||
case mysql.FieldTypeNameDateTime:
|
||||
values[i] = new(time.Time)
|
||||
case mysql.FieldTypeNameTime:
|
||||
values[i] = new(time.Duration)
|
||||
values[i] = new(string)
|
||||
case mysql.FieldTypeNameYear:
|
||||
values[i] = new(int16)
|
||||
case mysql.FieldTypeNameNULL:
|
||||
@ -307,6 +308,9 @@ func (s *stringStringScan) Update(rows *sql.Rows) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.time = null.FloatFromPtr(nil)
|
||||
s.value = null.FloatFromPtr(nil)
|
||||
|
||||
for i := 0; i < s.columnCount; i++ {
|
||||
if rb, ok := s.rowPtrs[i].(*sql.RawBytes); ok {
|
||||
s.rowValues[i] = string(*rb)
|
||||
|
@ -30,6 +30,7 @@ export class FormDropdownCtrl {
|
||||
optionCache: any;
|
||||
lookupText: boolean;
|
||||
|
||||
/** @ngInject **/
|
||||
constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
|
||||
this.inputElement = $element.find('input').first();
|
||||
this.linkElement = $element.find('a').first();
|
||||
|
@ -179,7 +179,7 @@ export class BackendSrv {
|
||||
}
|
||||
|
||||
// for Prometheus
|
||||
if (!err.data.message && _.isString(err.data.error)) {
|
||||
if (err.data && !err.data.message && _.isString(err.data.error)) {
|
||||
err.data.message = err.data.error;
|
||||
}
|
||||
|
||||
|
@ -25,3 +25,43 @@ export function tickStep(start: number, stop: number, count: number): number {
|
||||
|
||||
return stop < start ? -step1 : step1;
|
||||
}
|
||||
|
||||
export function getScaledDecimals(decimals, tick_size) {
|
||||
return decimals - Math.floor(Math.log(tick_size) / Math.LN10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate tick size based on min and max values, number of ticks and precision.
|
||||
* @param min Axis minimum
|
||||
* @param max Axis maximum
|
||||
* @param noTicks Number of ticks
|
||||
* @param tickDecimals Tick decimal precision
|
||||
*/
|
||||
export function getFlotTickSize(min: number, max: number, noTicks: number, tickDecimals: number) {
|
||||
var delta = (max - min) / noTicks,
|
||||
dec = -Math.floor(Math.log(delta) / Math.LN10),
|
||||
maxDec = tickDecimals;
|
||||
|
||||
var magn = Math.pow(10, -dec),
|
||||
norm = delta / magn, // norm is between 1.0 and 10.0
|
||||
size;
|
||||
|
||||
if (norm < 1.5) {
|
||||
size = 1;
|
||||
} else if (norm < 3) {
|
||||
size = 2;
|
||||
// special case for 2.5, requires an extra decimal
|
||||
if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
|
||||
size = 2.5;
|
||||
++dec;
|
||||
}
|
||||
} else if (norm < 7.5) {
|
||||
size = 5;
|
||||
} else {
|
||||
size = 10;
|
||||
}
|
||||
|
||||
size *= magn;
|
||||
|
||||
return size;
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ export class AlertNotificationEditCtrl {
|
||||
|
||||
this.backendSrv.post(`/api/alert-notifications/test`, payload)
|
||||
.then(res => {
|
||||
appEvents.emit('alert-succes', ['Test notification sent', '']);
|
||||
appEvents.emit('alert-success', ['Test notification sent', '']);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -247,9 +247,9 @@ export class DashboardModel {
|
||||
formatDate(date, format?) {
|
||||
date = moment.isMoment(date) ? date : moment(date);
|
||||
format = format || 'YYYY-MM-DD HH:mm:ss';
|
||||
this.timezone = this.getTimezone();
|
||||
let timezone = this.getTimezone();
|
||||
|
||||
return this.timezone === 'browser' ?
|
||||
return timezone === 'browser' ?
|
||||
moment(date).format(format) :
|
||||
moment.utc(date).format(format);
|
||||
}
|
||||
|
@ -29,8 +29,7 @@ const template = `
|
||||
ng-model="ctrl.message"
|
||||
ng-model-options="{allowInvalid: true}"
|
||||
ng-maxlength="this.max"
|
||||
autocomplete="off"
|
||||
required />
|
||||
autocomplete="off" />
|
||||
<small class="gf-form-hint-text muted" ng-cloak>
|
||||
<span ng-class="{'text-error': ctrl.saveForm.message.$invalid && ctrl.saveForm.message.$dirty }">
|
||||
{{ctrl.message.length || 0}}
|
||||
|
@ -56,6 +56,7 @@ export class TimePickerCtrl {
|
||||
if (moment.isMoment(timeRaw.to)) {
|
||||
timeRaw.to.local();
|
||||
}
|
||||
this.isUtc = false;
|
||||
} else {
|
||||
this.isUtc = true;
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
//import {coreModule} from 'app/core/core';
|
||||
import {DashboardModel} from '../dashboard/model';
|
||||
|
||||
export class MetricsTabCtrl {
|
||||
@ -32,7 +31,6 @@ export class MetricsTabCtrl {
|
||||
}
|
||||
|
||||
this.addQueryDropdown = {text: 'Add Query', value: null, fake: true};
|
||||
|
||||
// update next ref id
|
||||
this.panelCtrl.nextRefId = this.dashboard.getNextQueryLetter(this.panel);
|
||||
}
|
||||
@ -80,4 +78,3 @@ export function metricsTabDirective() {
|
||||
};
|
||||
}
|
||||
|
||||
//coreModule.directive('metricsTab', metricsTabDirective);
|
||||
|
@ -3,6 +3,7 @@
|
||||
import angular from 'angular';
|
||||
import coreModule from '../../core/core_module';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
class PlaylistSrv {
|
||||
private cancelPromise: any;
|
||||
@ -14,7 +15,13 @@ class PlaylistSrv {
|
||||
public isPlaying: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope: any, private $location: any, private $timeout: any, private backendSrv: any) { }
|
||||
constructor(
|
||||
private $rootScope: any,
|
||||
private $location: any,
|
||||
private $timeout: any,
|
||||
private backendSrv: any,
|
||||
private $routeParams: any
|
||||
) { }
|
||||
|
||||
next() {
|
||||
this.$timeout.cancel(this.cancelPromise);
|
||||
@ -22,14 +29,32 @@ class PlaylistSrv {
|
||||
var playedAllDashboards = this.index > this.dashboards.length - 1;
|
||||
|
||||
if (playedAllDashboards) {
|
||||
window.location.href = this.startUrl;
|
||||
} else {
|
||||
var dash = this.dashboards[this.index];
|
||||
this.$location.url('dashboard/' + dash.uri);
|
||||
|
||||
this.index++;
|
||||
this.cancelPromise = this.$timeout(() => this.next(), this.interval);
|
||||
window.location.href = this.getUrlWithKioskMode();
|
||||
return;
|
||||
}
|
||||
|
||||
var dash = this.dashboards[this.index];
|
||||
this.$location.url('dashboard/' + dash.uri);
|
||||
|
||||
this.index++;
|
||||
this.cancelPromise = this.$timeout(() => this.next(), this.interval);
|
||||
}
|
||||
|
||||
getUrlWithKioskMode() {
|
||||
const inKioskMode = document.body.classList.contains('page-kiosk-mode');
|
||||
|
||||
// check if should add kiosk query param
|
||||
if (inKioskMode && this.startUrl.indexOf('kiosk') === -1) {
|
||||
return this.startUrl + '?kiosk=true';
|
||||
}
|
||||
|
||||
// check if should remove kiosk query param
|
||||
if (!inKioskMode) {
|
||||
return this.startUrl.split("?")[0];
|
||||
}
|
||||
|
||||
// already has kiosk query param, just return startUrl
|
||||
return this.startUrl;
|
||||
}
|
||||
|
||||
prev() {
|
||||
@ -45,6 +70,10 @@ class PlaylistSrv {
|
||||
this.playlistId = playlistId;
|
||||
this.isPlaying = true;
|
||||
|
||||
if (this.$routeParams.kiosk) {
|
||||
appEvents.emit('toggle-kiosk-mode');
|
||||
}
|
||||
|
||||
this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => {
|
||||
this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => {
|
||||
this.dashboards = dashboards;
|
||||
|
@ -211,8 +211,8 @@ export default class InfluxDatasource {
|
||||
var params: any = {};
|
||||
|
||||
if (this.username) {
|
||||
params.username = this.username;
|
||||
params.password = this.password;
|
||||
params.u = this.username;
|
||||
params.u = this.password;
|
||||
}
|
||||
|
||||
if (options && options.database) {
|
||||
|
@ -46,8 +46,8 @@
|
||||
<li>$measurement = replaced with measurement name</li>
|
||||
<li>$1 - $9 = replaced with part of measurement name (if you separate your measurement name with dots)</li>
|
||||
<li>$col = replaced with column name</li>
|
||||
<li>$tag_hostname = replaced with the value of the hostname tag</li>
|
||||
<li>You can also use [[tag_hostname]] pattern replacement syntax</li>
|
||||
<li>$tag_exampletag = replaced with the value of the <i>exampletag</i> tag</li>
|
||||
<li>You can also use [[tag_exampletag]] pattern replacement syntax</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -145,14 +145,14 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
}
|
||||
|
||||
// add left axis labels
|
||||
if (panel.yaxes[0].label) {
|
||||
if (panel.yaxes[0].label && panel.yaxes[0].show) {
|
||||
var yaxisLabel = $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
|
||||
.text(panel.yaxes[0].label)
|
||||
.appendTo(elem);
|
||||
}
|
||||
|
||||
// add right axis labels
|
||||
if (panel.yaxes[1].label) {
|
||||
if (panel.yaxes[1].label && panel.yaxes[1].show) {
|
||||
var rightLabel = $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
|
||||
.text(panel.yaxes[1].label)
|
||||
.appendTo(elem);
|
||||
@ -422,21 +422,32 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
|
||||
function addXHistogramAxis(options, bucketSize) {
|
||||
let ticks, min, max;
|
||||
let defaultTicks = panelWidth / 50;
|
||||
|
||||
if (data.length && bucketSize) {
|
||||
ticks = _.map(data[0].data, point => point[0]);
|
||||
min = _.min(ticks);
|
||||
max = _.max(ticks);
|
||||
|
||||
// Adjust tick step
|
||||
let tickStep = bucketSize;
|
||||
let ticks_num = Math.floor((max - min) / tickStep);
|
||||
while (ticks_num > defaultTicks) {
|
||||
tickStep = tickStep * 2;
|
||||
ticks_num = Math.ceil((max - min) / tickStep);
|
||||
}
|
||||
|
||||
// Expand ticks for pretty view
|
||||
min = Math.max(0, _.min(ticks) - bucketSize);
|
||||
max = _.max(ticks) + bucketSize;
|
||||
min = Math.floor(min / tickStep) * tickStep;
|
||||
max = Math.ceil(max / tickStep) * tickStep;
|
||||
|
||||
ticks = [];
|
||||
for (let i = min; i <= max; i += bucketSize) {
|
||||
for (let i = min; i <= max; i += tickStep) {
|
||||
ticks.push(i);
|
||||
}
|
||||
} else {
|
||||
// Set defaults if no data
|
||||
ticks = panelWidth / 100;
|
||||
ticks = defaultTicks / 2;
|
||||
min = 0;
|
||||
max = 1;
|
||||
}
|
||||
@ -450,6 +461,9 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
label: "Histogram",
|
||||
ticks: ticks
|
||||
};
|
||||
|
||||
// Use 'short' format for histogram values
|
||||
configureAxisMode(options.xaxis, 'short');
|
||||
}
|
||||
|
||||
function addXTableAxis(options) {
|
||||
|
@ -38,9 +38,12 @@ export function convertValuesToHistogram(values: number[], bucketSize: number):
|
||||
}
|
||||
}
|
||||
|
||||
return _.map(histogram, (count, bound) => {
|
||||
let histogam_series = _.map(histogram, (count, bound) => {
|
||||
return [Number(bound), count];
|
||||
});
|
||||
|
||||
// Sort by Y axis values
|
||||
return _.sortBy(histogam_series, point => point[0]);
|
||||
}
|
||||
|
||||
function getBucketBound(value: number, bucketSize: number): number {
|
||||
|
@ -95,6 +95,8 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
|
||||
series: any;
|
||||
timeSrv: any;
|
||||
dataWarning: any;
|
||||
decimals: number;
|
||||
scaledDecimals: number;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, $injector, private $rootScope, timeSrv) {
|
||||
|
@ -15,6 +15,7 @@ export class HeatmapTooltip {
|
||||
tooltip: any;
|
||||
scope: any;
|
||||
dashboard: any;
|
||||
panelCtrl: any;
|
||||
panel: any;
|
||||
heatmapPanel: any;
|
||||
mouseOverBucket: boolean;
|
||||
@ -23,6 +24,7 @@ export class HeatmapTooltip {
|
||||
constructor(elem, scope) {
|
||||
this.scope = scope;
|
||||
this.dashboard = scope.ctrl.dashboard;
|
||||
this.panelCtrl = scope.ctrl;
|
||||
this.panel = scope.ctrl.panel;
|
||||
this.heatmapPanel = elem;
|
||||
this.mouseOverBucket = false;
|
||||
@ -85,8 +87,18 @@ export class HeatmapTooltip {
|
||||
|
||||
let tooltipTimeFormat = 'YYYY-MM-DD HH:mm:ss';
|
||||
let time = this.dashboard.formatDate(xData.x, tooltipTimeFormat);
|
||||
let decimals = this.panel.tooltipDecimals || 5;
|
||||
let valueFormatter = this.valueFormatter(decimals);
|
||||
|
||||
// Decimals override. Code from panel/graph/graph.ts
|
||||
let valueFormatter;
|
||||
if (_.isNumber(this.panel.tooltipDecimals)) {
|
||||
valueFormatter = this.valueFormatter(this.panel.tooltipDecimals, null);
|
||||
} else {
|
||||
// auto decimals
|
||||
// legend and tooltip gets one more decimal precision
|
||||
// than graph legend ticks
|
||||
let decimals = (this.panelCtrl.decimals || -1) + 1;
|
||||
valueFormatter = this.valueFormatter(decimals, this.panelCtrl.scaledDecimals + 2);
|
||||
}
|
||||
|
||||
let tooltipHtml = `<div class="graph-tooltip-time">${time}</div>
|
||||
<div class="heatmap-histogram"></div>`;
|
||||
@ -220,13 +232,10 @@ export class HeatmapTooltip {
|
||||
.style("top", top + "px");
|
||||
}
|
||||
|
||||
valueFormatter(decimals) {
|
||||
valueFormatter(decimals, scaledDecimals = null) {
|
||||
let format = this.panel.yAxis.format;
|
||||
return function(value) {
|
||||
if (_.isInteger(value)) {
|
||||
decimals = 0;
|
||||
}
|
||||
return kbn.valueFormats[format](value, decimals);
|
||||
return kbn.valueFormats[format](value, decimals, scaledDecimals);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import $ from 'jquery';
|
||||
import moment from 'moment';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import {appEvents, contextSrv} from 'app/core/core';
|
||||
import {tickStep} from 'app/core/utils/ticks';
|
||||
import {tickStep, getScaledDecimals, getFlotTickSize} from 'app/core/utils/ticks';
|
||||
import d3 from 'd3';
|
||||
import {HeatmapTooltip} from './heatmap_tooltip';
|
||||
import {convertToCards, mergeZeroBuckets} from './heatmap_data_converter';
|
||||
@ -100,10 +100,17 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
|
||||
let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
|
||||
let grafanaTimeFormatter = grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
|
||||
let timeFormat;
|
||||
let dashboardTimeZone = ctrl.dashboard.getTimezone();
|
||||
if (dashboardTimeZone === 'utc') {
|
||||
timeFormat = d3.utcFormat(grafanaTimeFormatter);
|
||||
} else {
|
||||
timeFormat = d3.timeFormat(grafanaTimeFormatter);
|
||||
}
|
||||
|
||||
let xAxis = d3.axisBottom(xScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(d3.timeFormat(grafanaTimeFormatter))
|
||||
.tickFormat(timeFormat)
|
||||
.tickPadding(X_AXIS_TICK_PADDING)
|
||||
.tickSize(chartHeight);
|
||||
|
||||
@ -131,7 +138,13 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
tick_interval = tickStep(y_min, y_max, ticks);
|
||||
ticks = Math.ceil((y_max - y_min) / tick_interval);
|
||||
|
||||
let decimals = panel.yAxis.decimals === null ? getPrecision(tick_interval) : panel.yAxis.decimals;
|
||||
let decimalsAuto = getPrecision(tick_interval);
|
||||
let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
|
||||
// Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
|
||||
let flot_tick_size = getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
|
||||
let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
|
||||
ctrl.decimals = decimals;
|
||||
ctrl.scaledDecimals = scaledDecimals;
|
||||
|
||||
// Set default Y min and max if no data
|
||||
if (_.isEmpty(data.buckets)) {
|
||||
@ -153,7 +166,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
|
||||
let yAxis = d3.axisLeft(yScale)
|
||||
.ticks(ticks)
|
||||
.tickFormat(tickValueFormatter(decimals))
|
||||
.tickFormat(tickValueFormatter(decimals, scaledDecimals))
|
||||
.tickSizeInner(0 - width)
|
||||
.tickSizeOuter(0)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
@ -213,7 +226,15 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
|
||||
let domain = yScale.domain();
|
||||
let tick_values = logScaleTickValues(domain, log_base);
|
||||
let decimals = panel.yAxis.decimals;
|
||||
|
||||
let decimalsAuto = getPrecision(y_min);
|
||||
let decimals = panel.yAxis.decimals || decimalsAuto;
|
||||
|
||||
// Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
|
||||
let flot_tick_size = getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
|
||||
let scaledDecimals = getScaledDecimals(decimals, flot_tick_size);
|
||||
ctrl.decimals = decimals;
|
||||
ctrl.scaledDecimals = scaledDecimals;
|
||||
|
||||
data.yAxis = {
|
||||
min: y_min,
|
||||
@ -223,7 +244,7 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
|
||||
let yAxis = d3.axisLeft(yScale)
|
||||
.tickValues(tick_values)
|
||||
.tickFormat(tickValueFormatter(decimals))
|
||||
.tickFormat(tickValueFormatter(decimals, scaledDecimals))
|
||||
.tickSizeInner(0 - width)
|
||||
.tickSizeOuter(0)
|
||||
.tickPadding(Y_AXIS_TICK_PADDING);
|
||||
@ -293,10 +314,10 @@ export default function link(scope, elem, attrs, ctrl) {
|
||||
return tickValues;
|
||||
}
|
||||
|
||||
function tickValueFormatter(decimals) {
|
||||
function tickValueFormatter(decimals, scaledDecimals = null) {
|
||||
let format = panel.yAxis.format;
|
||||
return function(value) {
|
||||
return kbn.valueFormats[format](value, decimals);
|
||||
return kbn.valueFormats[format](value, decimals, scaledDecimals);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -153,11 +153,11 @@ describe('grafanaHeatmap', function () {
|
||||
it('should draw correct X axis', function () {
|
||||
var xTicks = getTicks(ctx.element, ".axis-x");
|
||||
let expectedTicks = [
|
||||
formatLocalTime("01 Mar 2017 10:00:00"),
|
||||
formatLocalTime("01 Mar 2017 10:15:00"),
|
||||
formatLocalTime("01 Mar 2017 10:30:00"),
|
||||
formatLocalTime("01 Mar 2017 10:45:00"),
|
||||
formatLocalTime("01 Mar 2017 11:00:00")
|
||||
formatTime("01 Mar 2017 10:00:00"),
|
||||
formatTime("01 Mar 2017 10:15:00"),
|
||||
formatTime("01 Mar 2017 10:30:00"),
|
||||
formatTime("01 Mar 2017 10:45:00"),
|
||||
formatTime("01 Mar 2017 11:00:00")
|
||||
];
|
||||
expect(xTicks).to.eql(expectedTicks);
|
||||
});
|
||||
@ -204,7 +204,7 @@ describe('grafanaHeatmap', function () {
|
||||
|
||||
it('should draw correct Y axis', function () {
|
||||
var yTicks = getTicks(ctx.element, ".axis-y");
|
||||
expect(yTicks).to.eql(['1', '32', '1 K']);
|
||||
expect(yTicks).to.eql(['1', '32', '1.0 K']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -221,7 +221,7 @@ describe('grafanaHeatmap', function () {
|
||||
|
||||
it('should draw correct Y axis', function () {
|
||||
var yTicks = getTicks(ctx.element, ".axis-y");
|
||||
expect(yTicks).to.eql(['1', '1 K', '1 Mil']);
|
||||
expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -247,7 +247,7 @@ describe('grafanaHeatmap', function () {
|
||||
|
||||
it('should draw correct Y axis', function () {
|
||||
var yTicks = getTicks(ctx.element, ".axis-y");
|
||||
expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1 hour']);
|
||||
expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']);
|
||||
});
|
||||
});
|
||||
|
||||
@ -261,7 +261,7 @@ function getTicks(element, axisSelector) {
|
||||
}).get();
|
||||
}
|
||||
|
||||
function formatLocalTime(timeStr) {
|
||||
function formatTime(timeStr) {
|
||||
let format = "HH:mm";
|
||||
return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').local().format(format);
|
||||
return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format);
|
||||
}
|
||||
|
@ -33,27 +33,30 @@
|
||||
<h5 class="section-heading">Type</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Type</label>
|
||||
<label class="gf-form-label width-11">Type</label>
|
||||
<div class="gf-form-select-wrapper width-10">
|
||||
<select class="gf-form-input" ng-model="style.type" ng-options="c.value as c.text for c in editor.columnTypes" ng-change="editor.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="style.type === 'date'">
|
||||
<label class="gf-form-label width-8">Date Format</label>
|
||||
<label class="gf-form-label width-11">Date Format</label>
|
||||
<metric-segment-model property="style.dateFormat" options="editor.dateFormats" on-change="editor.render()" custom="true"></metric-segment-model>
|
||||
</div>
|
||||
|
||||
<div ng-if="style.type === 'string'">
|
||||
<gf-form-switch class="gf-form" label-class="width-8" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Sanitize HTML" checked="style.sanitize" change="editor.render()"></gf-form-switch>
|
||||
</div>
|
||||
<div ng-if="style.type === 'string'">
|
||||
<gf-form-switch class="gf-form" label-class="width-11" ng-if="style.type === 'string'" label="Preserve Formatting" checked="style.preserveFormat" change="editor.render()"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<div ng-if="style.type === 'number'">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Unit</label>
|
||||
<label class="gf-form-label width-11">Unit</label>
|
||||
<div class="gf-form-dropdown-typeahead width-10" ng-model="style.unit" dropdown-typeahead2="editor.unitFormats" dropdown-typeahead-on-select="editor.setUnitFormat(style, $subItem)"></div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-8">Decimals</label>
|
||||
<label class="gf-form-label width-11">Decimals</label>
|
||||
<input type="number" class="gf-form-input width-4" data-placement="right" ng-model="style.decimals" ng-change="editor.render()" ng-model-onblur>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -130,6 +130,7 @@ export class TableRenderer {
|
||||
renderCell(columnIndex, value, addWidthHack = false) {
|
||||
value = this.formatColumnValue(columnIndex, value);
|
||||
var style = '';
|
||||
var cellClass = '';
|
||||
if (this.colorState.cell) {
|
||||
style = ' style="background-color:' + this.colorState.cell + ';color: white"';
|
||||
this.colorState.cell = null;
|
||||
@ -153,7 +154,12 @@ export class TableRenderer {
|
||||
this.table.columns[columnIndex].hidden = false;
|
||||
}
|
||||
|
||||
return '<td' + style + '>' + value + widthHack + '</td>';
|
||||
var columnStyle = this.table.columns[columnIndex].style;
|
||||
if (columnStyle && columnStyle.preserveFormat) {
|
||||
cellClass = ' class="table-panel-cell-pre" ';
|
||||
}
|
||||
|
||||
return '<td' + cellClass + style + '>' + value + widthHack + '</td>';
|
||||
}
|
||||
|
||||
render(page) {
|
||||
|
@ -117,6 +117,11 @@ $gf-form-margin: 0.25rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
// text areas should be scrollable
|
||||
@at-root textarea#{&} {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
// Unstyle the caret on `<select>`s in IE10+.
|
||||
&::-ms-expand {
|
||||
background-color: transparent;
|
||||
|
@ -72,6 +72,10 @@
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&.table-panel-cell-pre {
|
||||
white-space: pre;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@
|
||||
padding: 0;
|
||||
|
||||
.tabbed-view-header {
|
||||
/* padding: 0; */
|
||||
background-color: $body-bg;
|
||||
padding: 1.5em 1rem 0 1rem;
|
||||
}
|
||||
@ -48,7 +47,7 @@
|
||||
}
|
||||
|
||||
.tabbed-view-body {
|
||||
padding: $spacer*2;
|
||||
padding: $spacer*2 $spacer;
|
||||
min-height: 250px;
|
||||
}
|
||||
|
||||
|
33
public/test/specs/backend_srv-specs.js
Normal file
33
public/test/specs/backend_srv-specs.js
Normal file
@ -0,0 +1,33 @@
|
||||
define([
|
||||
'app/core/config',
|
||||
'app/core/services/backend_srv'
|
||||
], function() {
|
||||
'use strict';
|
||||
|
||||
describe('backend_srv', function() {
|
||||
var _backendSrv;
|
||||
var _http;
|
||||
var _httpBackend;
|
||||
|
||||
beforeEach(module('grafana.core'));
|
||||
beforeEach(module('grafana.services'));
|
||||
beforeEach(inject(function ($httpBackend, $http, backendSrv) {
|
||||
_httpBackend = $httpBackend;
|
||||
_http = $http;
|
||||
_backendSrv = backendSrv;
|
||||
}));
|
||||
|
||||
describe('when handling errors', function() {
|
||||
it('should return the http status code', function(done) {
|
||||
_httpBackend.whenGET('gateway-error').respond(502);
|
||||
_backendSrv.datasourceRequest({
|
||||
url: 'gateway-error'
|
||||
}).catch(function(err) {
|
||||
expect(err.status).to.be(502);
|
||||
done();
|
||||
});
|
||||
_httpBackend.flush();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
3
tests/config-files/override_windows.ini
Normal file
3
tests/config-files/override_windows.ini
Normal file
@ -0,0 +1,3 @@
|
||||
[paths]
|
||||
data = c:\tmp\override
|
||||
|
Loading…
Reference in New Issue
Block a user