mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into enterprise-docs
This commit is contained in:
@@ -206,6 +206,10 @@ jobs:
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}"
|
||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cd packaging/docker && ./build-enterprise.sh "master"
|
||||
|
||||
|
||||
grafana-docker-pr:
|
||||
docker:
|
||||
@@ -230,6 +234,9 @@ jobs:
|
||||
- run: docker info
|
||||
- run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker
|
||||
- run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}"
|
||||
- run: rm packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cp enterprise-dist/grafana-enterprise-*.linux-amd64.tar.gz packaging/docker/grafana-latest.linux-x64.tar.gz
|
||||
- run: cd packaging/docker && ./build-enterprise.sh "${CIRCLE_TAG}"
|
||||
|
||||
build-enterprise:
|
||||
docker:
|
||||
@@ -238,8 +245,17 @@ jobs:
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: build, test and package grafana enterprise
|
||||
command: './scripts/build/build_enterprise.sh'
|
||||
name: prepare build tools
|
||||
command: '/tmp/bootstrap.sh'
|
||||
- run:
|
||||
name: checkout enterprise
|
||||
command: './scripts/build/prepare-enterprise.sh'
|
||||
- run:
|
||||
name: test enterprise
|
||||
command: 'go test ./pkg/extensions/...'
|
||||
- run:
|
||||
name: build and package enterprise
|
||||
command: './scripts/build/build.sh -enterprise'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
@@ -254,6 +270,53 @@ jobs:
|
||||
paths:
|
||||
- enterprise-dist/grafana-enterprise*
|
||||
|
||||
build-all-enterprise:
|
||||
docker:
|
||||
- image: grafana/build-container:1.2.0
|
||||
working_directory: /go/src/github.com/grafana/grafana
|
||||
steps:
|
||||
- checkout
|
||||
- run:
|
||||
name: prepare build tools
|
||||
command: '/tmp/bootstrap.sh'
|
||||
- run:
|
||||
name: checkout enterprise
|
||||
command: './scripts/build/prepare-enterprise.sh'
|
||||
- restore_cache:
|
||||
key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
|
||||
- run:
|
||||
name: download phantomjs binaries
|
||||
command: './scripts/build/download-phantomjs.sh'
|
||||
- save_cache:
|
||||
key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }}
|
||||
paths:
|
||||
- /tmp/phantomjs
|
||||
- run:
|
||||
name: test enterprise
|
||||
command: 'go test ./pkg/extensions/...'
|
||||
- run:
|
||||
name: build and package grafana
|
||||
command: './scripts/build/build-all.sh -enterprise'
|
||||
- run:
|
||||
name: sign packages
|
||||
command: './scripts/build/sign_packages.sh'
|
||||
- run:
|
||||
name: verify signed packages
|
||||
command: |
|
||||
mkdir -p ~/.rpmdb/pubkeys
|
||||
curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key
|
||||
./scripts/build/verify_signed_packages.sh dist/*.rpm
|
||||
- run:
|
||||
name: sha-sum packages
|
||||
command: 'go run build.go sha-dist'
|
||||
- run:
|
||||
name: move enterprise packages into their own folder
|
||||
command: 'mv dist enterprise-dist'
|
||||
- persist_to_workspace:
|
||||
root: .
|
||||
paths:
|
||||
- enterprise-dist/grafana-enterprise*
|
||||
|
||||
deploy-enterprise-master:
|
||||
docker:
|
||||
- image: circleci/python:2.7-stretch
|
||||
@@ -267,6 +330,19 @@ jobs:
|
||||
name: deploy to s3
|
||||
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
|
||||
|
||||
deploy-enterprise-release:
|
||||
docker:
|
||||
- image: circleci/python:2.7-stretch
|
||||
steps:
|
||||
- attach_workspace:
|
||||
at: .
|
||||
- run:
|
||||
name: install awscli
|
||||
command: 'sudo pip install awscli'
|
||||
- run:
|
||||
name: deploy to s3
|
||||
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release'
|
||||
|
||||
deploy-master:
|
||||
docker:
|
||||
- image: circleci/python:2.7-stretch
|
||||
@@ -313,7 +389,7 @@ workflows:
|
||||
jobs:
|
||||
- build-all:
|
||||
filters: *filter-only-master
|
||||
- build-enterprise:
|
||||
- build-all-enterprise:
|
||||
filters: *filter-only-master
|
||||
- codespell:
|
||||
filters: *filter-only-master
|
||||
@@ -340,6 +416,7 @@ workflows:
|
||||
- grafana-docker-master:
|
||||
requires:
|
||||
- build-all
|
||||
- build-all-enterprise
|
||||
- test-backend
|
||||
- test-frontend
|
||||
- codespell
|
||||
@@ -356,13 +433,15 @@ workflows:
|
||||
- gometalinter
|
||||
- mysql-integration-test
|
||||
- postgres-integration-test
|
||||
- build-enterprise
|
||||
- build-all-enterprise
|
||||
filters: *filter-only-master
|
||||
|
||||
release:
|
||||
jobs:
|
||||
- build-all:
|
||||
filters: *filter-only-release
|
||||
- build-all-enterprise:
|
||||
filters: *filter-only-release
|
||||
- codespell:
|
||||
filters: *filter-only-release
|
||||
- gometalinter:
|
||||
@@ -385,6 +464,17 @@ workflows:
|
||||
- mysql-integration-test
|
||||
- postgres-integration-test
|
||||
filters: *filter-only-release
|
||||
- deploy-enterprise-release:
|
||||
requires:
|
||||
- build-all
|
||||
- build-all-enterprise
|
||||
- test-backend
|
||||
- test-frontend
|
||||
- codespell
|
||||
- gometalinter
|
||||
- mysql-integration-test
|
||||
- postgres-integration-test
|
||||
filters: *filter-only-release
|
||||
- grafana-docker-release:
|
||||
requires:
|
||||
- build-all
|
||||
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -2,18 +2,45 @@
|
||||
|
||||
### New Features
|
||||
|
||||
* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat)
|
||||
* **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset)
|
||||
* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro)
|
||||
* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||
* **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
||||
* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||
|
||||
### Breaking changes
|
||||
|
||||
* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited)
|
||||
|
||||
# 5.3.3 (unreleased)
|
||||
|
||||
* **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769)
|
||||
|
||||
# 5.3.2 (2018-10-24)
|
||||
|
||||
* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm)
|
||||
* **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Cloudwatch**: Fix check for invalid percentile statistics [#13633](https://github.com/grafana/grafana/issues/13633), thx [@apalaniuk](https://github.com/apalaniuk)
|
||||
* **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Stackdriver**: stackdriver user-metrics duplicated response when multiple resource types [#13691](https://github.com/grafana/grafana/issues/13691)
|
||||
* **Variables**: Fix text box template variable doesn't work properly without a default value [#13666](https://github.com/grafana/grafana/issues/13666)
|
||||
* **Variables**: Fix variable dependency check when using `${var}` format [#13600](https://github.com/grafana/grafana/issues/13600)
|
||||
* **Dashboard**: Fix kiosk=1 url parameter should put dashboard in kiosk mode [#13764](https://github.com/grafana/grafana/pull/13764)
|
||||
* **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f)
|
||||
* **Provisioning**: Fix deleting provisioned dashboard folder should cleanup provisioning meta data [#13280](https://github.com/grafana/grafana/issues/13280)
|
||||
|
||||
### Minor
|
||||
|
||||
* **Docker**: adds curl back into the docker image for utility. [#13794](https://github.com/grafana/grafana/pull/13794)
|
||||
|
||||
# 5.3.1 (2018-10-16)
|
||||
|
||||
* **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616)
|
||||
|
||||
@@ -9,12 +9,17 @@ module.exports = function (grunt) {
|
||||
destDir: 'dist',
|
||||
tempDir: 'tmp',
|
||||
platform: process.platform.replace('win32', 'windows'),
|
||||
enterprise: false,
|
||||
};
|
||||
|
||||
if (grunt.option('platform')) {
|
||||
config.platform = grunt.option('platform');
|
||||
}
|
||||
|
||||
if (grunt.option('enterprise')) {
|
||||
config.enterprise = true;
|
||||
}
|
||||
|
||||
if (grunt.option('arch')) {
|
||||
config.arch = grunt.option('arch');
|
||||
} else {
|
||||
|
||||
10
Makefile
10
Makefile
@@ -5,8 +5,7 @@ all: deps build
|
||||
deps-go:
|
||||
go run build.go setup
|
||||
|
||||
deps-js:
|
||||
yarn install --pure-lockfile --no-progress
|
||||
deps-js: node_modules
|
||||
|
||||
deps: deps-js
|
||||
|
||||
@@ -43,3 +42,10 @@ test: test-go test-js
|
||||
|
||||
run:
|
||||
./bin/grafana-server
|
||||
|
||||
clean:
|
||||
rm -rf node_modules
|
||||
rm -rf public/build
|
||||
|
||||
node_modules: package.json yarn.lock
|
||||
yarn install --pure-lockfile --no-progress
|
||||
|
||||
30
README.md
30
README.md
@@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- Go 1.11
|
||||
- Go (Latest Stable)
|
||||
- NodeJS LTS
|
||||
|
||||
### Building the backend
|
||||
@@ -69,15 +69,27 @@ bra run
|
||||
|
||||
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
|
||||
|
||||
### Building a docker image (on linux/amd64)
|
||||
### Building a Docker image
|
||||
|
||||
This builds a docker image from your local sources:
|
||||
There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
|
||||
|
||||
Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
|
||||
|
||||
#### Building on linux/amd64 (fast)
|
||||
|
||||
1. Build the frontend `go run build.go build-frontend`
|
||||
2. Build the docker image `make build-docker-dev`
|
||||
|
||||
The resulting image will be tagged as `grafana/grafana:dev`
|
||||
|
||||
#### Building anywhere (slower)
|
||||
|
||||
Choose this option to build on platforms other than linux/amd64 and/or not have to setup the Grafana development environment.
|
||||
|
||||
1. `make build-docker-full` or `docker build -t grafana/grafana:dev .`
|
||||
|
||||
The resulting image will be tagged as `grafana/grafana:dev`
|
||||
|
||||
### Dev config
|
||||
|
||||
Create a custom.ini in the conf directory to override default configuration options.
|
||||
@@ -113,18 +125,6 @@ GRAFANA_TEST_DB=mysql go test ./pkg/...
|
||||
GRAFANA_TEST_DB=postgres go test ./pkg/...
|
||||
```
|
||||
|
||||
## Building custom docker image
|
||||
|
||||
You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself.
|
||||
```bash
|
||||
git clone https://github.com/grafana/grafana
|
||||
cd grafana
|
||||
docker build -t grafana:dev .
|
||||
docker run -d --name=grafana -p 3000:3000 grafana:dev
|
||||
```
|
||||
|
||||
Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`).
|
||||
|
||||
## Contribute
|
||||
|
||||
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
||||
|
||||
89
UPGRADING_DEPENDENCIES.md
Normal file
89
UPGRADING_DEPENDENCIES.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Guide to Upgrading Dependencies
|
||||
|
||||
Upgrading Go or Node.js requires making changes in many different files. See below for a list and explanation for each.
|
||||
|
||||
## Go
|
||||
|
||||
- CircleCi
|
||||
- `grafana/build-container`
|
||||
- Appveyor
|
||||
- Dockerfile
|
||||
|
||||
## Node.js
|
||||
|
||||
- CircleCI
|
||||
- `grafana/build-container`
|
||||
- Appveyor
|
||||
- Dockerfile
|
||||
|
||||
## Go Dependencies
|
||||
|
||||
Updated using `dep`.
|
||||
|
||||
- `Gopkg.toml`
|
||||
- `Gopkg.lock`
|
||||
|
||||
## Node.js Dependencies
|
||||
|
||||
Updated using `yarn`.
|
||||
|
||||
- `package.json`
|
||||
|
||||
## Where to make changes
|
||||
|
||||
### CircleCI
|
||||
|
||||
Our builds run on CircleCI through our build script.
|
||||
|
||||
#### Files
|
||||
|
||||
- `.circleci/config.yml`.
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- nodejs
|
||||
- golang
|
||||
- grafana/build-container (our custom docker build container)
|
||||
|
||||
### grafana/build-container
|
||||
|
||||
The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
|
||||
|
||||
Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- fpm
|
||||
- nodejs
|
||||
- golang
|
||||
- crosscompiling (several compilers)
|
||||
|
||||
### Appveyor
|
||||
|
||||
Master and release builds trigger test runs on Appveyors build environment so that tests will run on Windows.
|
||||
|
||||
#### Files:
|
||||
|
||||
- `appveyor.yml`
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- nodejs
|
||||
- golang
|
||||
|
||||
### Dockerfile
|
||||
|
||||
There is a Docker build for Grafana in the root of the project that allows anyone to build Grafana just using Docker.
|
||||
|
||||
#### Files
|
||||
|
||||
- `Dockerfile`
|
||||
|
||||
#### Dependencies
|
||||
|
||||
- nodejs
|
||||
- golang
|
||||
|
||||
### Local developer environments
|
||||
|
||||
Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.
|
||||
13
build.go
13
build.go
@@ -403,6 +403,10 @@ func gruntBuildArg(task string) []string {
|
||||
if phjsToRelease != "" {
|
||||
args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease))
|
||||
}
|
||||
if enterprise {
|
||||
args = append(args, "--enterprise")
|
||||
}
|
||||
|
||||
args = append(args, fmt.Sprintf("--platform=%v", goos))
|
||||
|
||||
return args
|
||||
@@ -467,6 +471,7 @@ func ldflags() string {
|
||||
b.WriteString(fmt.Sprintf(" -X main.version=%s", version))
|
||||
b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha()))
|
||||
b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp()))
|
||||
b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch()))
|
||||
return b.String()
|
||||
}
|
||||
|
||||
@@ -514,6 +519,14 @@ func setBuildEnv() {
|
||||
}
|
||||
}
|
||||
|
||||
func getGitBranch() string {
|
||||
v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD")
|
||||
if err != nil {
|
||||
return "master"
|
||||
}
|
||||
return string(v)
|
||||
}
|
||||
|
||||
func getGitSha() string {
|
||||
v, err := runError("git", "rev-parse", "--short", "HEAD")
|
||||
if err != nil {
|
||||
|
||||
@@ -554,3 +554,6 @@ container_name =
|
||||
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||
server_url =
|
||||
callback_url =
|
||||
|
||||
[panels]
|
||||
enable_alpha = false
|
||||
|
||||
@@ -927,6 +927,123 @@
|
||||
"title": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
"dashLength": 10,
|
||||
"dashes": false,
|
||||
"datasource": "gdev-testdata",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"fill": 0,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 16,
|
||||
"x": 0,
|
||||
"y": 44
|
||||
},
|
||||
"id": 21,
|
||||
"legend": {
|
||||
"avg": false,
|
||||
"current": false,
|
||||
"max": false,
|
||||
"min": false,
|
||||
"show": true,
|
||||
"total": false,
|
||||
"values": false
|
||||
},
|
||||
"lines": true,
|
||||
"linewidth": 2,
|
||||
"links": [],
|
||||
"nullPointMode": "null",
|
||||
"percentage": false,
|
||||
"pointradius": 5,
|
||||
"points": false,
|
||||
"renderer": "flot",
|
||||
"seriesOverrides": [
|
||||
{
|
||||
"alias": "C-series",
|
||||
"steppedLine": true
|
||||
}
|
||||
],
|
||||
"spaceLength": 10,
|
||||
"stack": false,
|
||||
"steppedLine": false,
|
||||
"targets": [
|
||||
{
|
||||
"alias": "",
|
||||
"hide": false,
|
||||
"refId": "B",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "1,null,40,null,90,null,null,100,null,null,100,null,null,80,null",
|
||||
"target": ""
|
||||
},
|
||||
{
|
||||
"alias": "",
|
||||
"hide": false,
|
||||
"refId": "C",
|
||||
"scenarioId": "csv_metric_values",
|
||||
"stringInput": "20,null40,null,null,50,null,70,null,100,null,10,null,30,null",
|
||||
"target": ""
|
||||
}
|
||||
],
|
||||
"thresholds": [],
|
||||
"timeFrom": null,
|
||||
"timeShift": null,
|
||||
"title": "Null between points",
|
||||
"tooltip": {
|
||||
"msResolution": false,
|
||||
"shared": true,
|
||||
"sort": 0,
|
||||
"value_type": "cumulative"
|
||||
},
|
||||
"type": "graph",
|
||||
"xaxis": {
|
||||
"buckets": null,
|
||||
"mode": "time",
|
||||
"name": null,
|
||||
"show": true,
|
||||
"values": []
|
||||
},
|
||||
"yaxes": [
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
},
|
||||
{
|
||||
"format": "short",
|
||||
"label": null,
|
||||
"logBase": 1,
|
||||
"max": null,
|
||||
"min": null,
|
||||
"show": true
|
||||
}
|
||||
],
|
||||
"yaxis": {
|
||||
"align": false,
|
||||
"alignLevel": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"content": "Left is showing null between values for a normal line graph and staircase graph. Orphaned data points should be rendered as points",
|
||||
"editable": true,
|
||||
"error": false,
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 8,
|
||||
"x": 16,
|
||||
"y": 44
|
||||
},
|
||||
"id": 22,
|
||||
"links": [],
|
||||
"mode": "markdown",
|
||||
"title": "",
|
||||
"type": "text"
|
||||
},
|
||||
{
|
||||
"aliasColors": {},
|
||||
"bars": false,
|
||||
@@ -939,7 +1056,7 @@
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 44
|
||||
"y": 51
|
||||
},
|
||||
"id": 20,
|
||||
"legend": {
|
||||
@@ -1024,7 +1141,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 51
|
||||
"y": 58
|
||||
},
|
||||
"id": 16,
|
||||
"legend": {
|
||||
@@ -1127,7 +1244,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 51
|
||||
"y": 58
|
||||
},
|
||||
"id": 17,
|
||||
"legend": {
|
||||
@@ -1266,7 +1383,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 58
|
||||
"y": 65
|
||||
},
|
||||
"id": 18,
|
||||
"legend": {
|
||||
@@ -1370,7 +1487,7 @@
|
||||
"h": 7,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 58
|
||||
"y": 65
|
||||
},
|
||||
"id": 19,
|
||||
"legend": {
|
||||
@@ -1554,5 +1671,5 @@
|
||||
"timezone": "browser",
|
||||
"title": "Panel Tests - Graph",
|
||||
"uid": "5SdHCadmz",
|
||||
"version": 3
|
||||
"version": 1
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
|
||||
| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) |
|
||||
| timeField | string | Elasticsearch | Which field that should be used as timestamp |
|
||||
| interval | string | Elasticsearch | Index date time format |
|
||||
| interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' |
|
||||
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
||||
| assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
|
||||
| defaultRegion | string | Cloudwatch | AWS region |
|
||||
|
||||
@@ -140,7 +140,7 @@ In DingTalk PC Client:
|
||||
|
||||
6. There will be a Webhook URL in the panel, looks like this: https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx. Copy this URL to the grafana Dingtalk setting page and then click "finish".
|
||||
|
||||
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `text` message type is supported.
|
||||
Dingtalk supports the following "message type": `text`, `link` and `markdown`. Only the `link` message type is supported.
|
||||
|
||||
### Kafka
|
||||
|
||||
|
||||
@@ -100,12 +100,12 @@ display name, especially if the display name contains spaces or special
|
||||
characters. Make sure you always use the group or subgroup name as it appears
|
||||
in the URL of the group or subgroup.
|
||||
|
||||
Here's a complete example with `alloed_sign_up` enabled, and access limited to
|
||||
Here's a complete example with `allow_sign_up` enabled, and access limited to
|
||||
the `example` and `foo/bar` groups:
|
||||
|
||||
```ini
|
||||
[auth.gitlab]
|
||||
enabled = false
|
||||
enabled = true
|
||||
allow_sign_up = true
|
||||
client_id = GITLAB_APPLICATION_ID
|
||||
client_secret = GITLAB_SECRET
|
||||
|
||||
@@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu
|
||||
## IAM Policies
|
||||
|
||||
Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
|
||||
and EC2 tags/instances. You can attach these permissions to IAM roles and
|
||||
and EC2 tags/instances/regions. You can attach these permissions to IAM roles and
|
||||
utilize Grafana's built-in support for assuming roles.
|
||||
|
||||
Here is a minimal policy example:
|
||||
@@ -65,11 +65,12 @@ Here is a minimal policy example:
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowReadingTagsFromEC2",
|
||||
"Sid": "AllowReadingTagsInstancesRegionsFromEC2",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"ec2:DescribeTags",
|
||||
"ec2:DescribeInstances"
|
||||
"ec2:DescribeInstances",
|
||||
"ec2:DescribeRegions"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ Grafana ships with built-in support for Google Stackdriver. Just add it as a dat
|
||||
|
||||
## Authentication
|
||||
|
||||
### Service Account Credentials - Private Key File
|
||||
There are two ways to authenticate the Stackdriver plugin - either by uploading a Google JWT file, or by automatically retrieving credentials from Google metadata server. The latter option is only available when running Grafana on GCE virtual machine.
|
||||
|
||||
### Using a Google Service Account Key File
|
||||
|
||||
To authenticate with the Stackdriver API, you need to create a Google Cloud Platform (GCP) Service Account for the Project you want to show data for. A Grafana datasource integrates with one GCP Project. If you want to visualize data from multiple GCP Projects then you need to create one datasource per GCP Project.
|
||||
|
||||
@@ -74,6 +76,16 @@ Click on the links above and click the `Enable` button:
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v53/stackdriver_grafana_key_uploaded.png" class="docs-image--no-shadow" caption="Service key file is uploaded to Grafana" >}}
|
||||
|
||||
### Using GCE Default Service Account
|
||||
|
||||
If Grafana is running on a Google Compute Engine (GCE) virtual machine, it is possible for Grafana to automatically retrieve default credentials from the metadata server. This has the advantage of not needing to generate a private key file for the service account and also not having to upload the file to Grafana. However for this to work, there are a few preconditions that need to be met.
|
||||
|
||||
1. First of all, you need to create a Service Account that can be used by the GCE virtual machine. See detailed instructions on how to do that [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#createanewserviceaccount).
|
||||
2. Make sure the GCE virtual machine instance is being run as the service account that you just created. See instructions [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances#using).
|
||||
3. Allow access to the `Stackdriver Monitoring API` scope. See instructions [here](changeserviceaccountandscopes).
|
||||
|
||||
Read more about creating and enabling service accounts for GCE VM instances [here](https://cloud.google.com/compute/docs/access/create-enable-service-accounts-for-instances).
|
||||
|
||||
## Metric Query Editor
|
||||
|
||||
{{< docs-imagebox img="/img/docs/v53/stackdriver_query_editor.png" max-width= "400px" class="docs-image--right" >}}
|
||||
@@ -144,6 +156,16 @@ Example Alias By: `{{metric.type}} - {{metric.labels.instance_name}}`
|
||||
|
||||
Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod`
|
||||
|
||||
It is also possible to resolve the name of the Monitored Resource Type.
|
||||
|
||||
| Alias Pattern Format | Description | Example Result |
|
||||
| ------------------------ | ------------------------------------------------| ---------------- |
|
||||
| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` |
|
||||
|
||||
Example Alias By: `{{resource.type}} - {{metric.type}}`
|
||||
|
||||
Example Result: `gce_instance - compute.googleapis.com/instance/cpu/usage_time`
|
||||
|
||||
## Templating
|
||||
|
||||
Instead of hard-coding things like server, application and sensor name in you metric queries you can use variables in their place.
|
||||
@@ -194,7 +216,7 @@ Example Result: `monitoring.googleapis.com/uptime_check/http_status has this val
|
||||
|
||||
It's now possible to configure datasources using config files with Grafana's provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](/administration/provisioning/#datasources)
|
||||
|
||||
Here is a provisioning example for this datasource.
|
||||
Here is a provisioning example using the JWT (Service Account key file) authentication type.
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
@@ -206,6 +228,8 @@ datasources:
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
authenticationType: jwt
|
||||
defaultProject: my-project-name
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
@@ -214,3 +238,16 @@ datasources:
|
||||
yA+23427282348234=
|
||||
-----END PRIVATE KEY-----
|
||||
```
|
||||
|
||||
Here is a provisioning example using GCE Default Service Account authentication.
|
||||
|
||||
```yaml
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Stackdriver
|
||||
type: stackdriver
|
||||
access: proxy
|
||||
jsonData:
|
||||
authenticationType: gce
|
||||
```
|
||||
|
||||
@@ -28,7 +28,7 @@ installation.
|
||||
```bash
|
||||
wget <debian package url>
|
||||
sudo apt-get install -y adduser libfontconfig
|
||||
sudo dpkg -i grafana_5.1.4_amd64.deb
|
||||
sudo dpkg -i grafana_<version>_amd64.deb
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
@@ -87,7 +87,7 @@ docker run \
|
||||
|
||||
## Building a custom Grafana image with pre-installed plugins
|
||||
|
||||
In the [grafana-docker](https://github.com/grafana/grafana-docker/) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
|
||||
In the [grafana-docker](https://github.com/grafana/grafana/tree/master/packaging/docker) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments.
|
||||
|
||||
Example of how to build and run:
|
||||
```bash
|
||||
@@ -103,6 +103,21 @@ docker run \
|
||||
grafana:latest-with-plugins
|
||||
```
|
||||
|
||||
## Installing Plugins from other sources
|
||||
|
||||
> Only available in Grafana v5.3.1+
|
||||
|
||||
It's possible to install plugins from custom url:s by specifying the url like this: `GF_INSTALL_PLUGINS=<url to plugin zip>;<plugin name>`
|
||||
|
||||
```bash
|
||||
docker run \
|
||||
-d \
|
||||
-p 3000:3000 \
|
||||
--name=grafana \
|
||||
-e "GF_INSTALL_PLUGINS=http://plugin-domain.com/my-custom-plugin.zip;custom-plugin" \
|
||||
grafana/grafana
|
||||
```
|
||||
|
||||
## Configuring AWS Credentials for CloudWatch Support
|
||||
|
||||
```bash
|
||||
|
||||
@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
|
||||
|
||||
## Dependencies
|
||||
|
||||
- [Go 1.11](https://golang.org/dl/)
|
||||
- [Go (Latest Stable)](https://golang.org/dl/)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS LTS](https://nodejs.org/download/)
|
||||
- node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
+++
|
||||
title = "Tutorials"
|
||||
type = "docs"
|
||||
[menu.docs]
|
||||
identifier = "tutorials"
|
||||
weight = 6
|
||||
@@ -11,7 +12,11 @@ This section of the docs contains a series for tutorials and stack setup guides.
|
||||
|
||||
## Articles
|
||||
|
||||
- [How to integrate Hubot with Grafana](hubot_howto.md)
|
||||
- [Running Grafana behind a reverse proxy]({{< relref "behind_proxy.md" >}})
|
||||
- [API Tutorial: How To Create API Tokens And Dashboards For A Specific Organization]({{< relref "api_org_token_howto.md" >}})
|
||||
- [How to Use IIS with URL Rewrite as a Reverse Proxy for Grafana on Windows]({{< relref "iis.md" >}})
|
||||
- [How to integrate Hubot with Grafana]({{< relref "hubot_howto.md" >}})
|
||||
- [How to setup Grafana for high availability]({{< relref "ha_setup.md" >}})
|
||||
|
||||
## External links
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"stable": "5.3.1",
|
||||
"testing": "5.3.1"
|
||||
"stable": "5.3.2",
|
||||
"testing": "5.3.2"
|
||||
}
|
||||
|
||||
@@ -160,6 +160,7 @@
|
||||
"react-redux": "^5.0.7",
|
||||
"react-select": "2.1.0",
|
||||
"react-sizeme": "^2.3.6",
|
||||
"react-table": "^6.8.6",
|
||||
"react-transition-group": "^2.2.1",
|
||||
"redux": "^4.0.0",
|
||||
"redux-logger": "^3.0.6",
|
||||
|
||||
@@ -25,7 +25,7 @@ ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bi
|
||||
|
||||
WORKDIR $GF_PATHS_HOME
|
||||
|
||||
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \
|
||||
RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates curl && \
|
||||
apt-get autoremove -y && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
|
||||
10
packaging/docker/build-enterprise.sh
Executable file
10
packaging/docker/build-enterprise.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
_grafana_tag=$1
|
||||
_docker_repo=${2:-grafana/grafana-enterprise}
|
||||
|
||||
docker build \
|
||||
--tag "${_docker_repo}:${_grafana_tag}"\
|
||||
--no-cache=true \
|
||||
.
|
||||
@@ -1,29 +0,0 @@
|
||||
# New Grafana Release Processes
|
||||
|
||||
## Building release packages
|
||||
|
||||
1) Update package.json so that it has the right version.
|
||||
2) Create a git tag for the release: `git tag -a v3.0.4 -m "3.0.4 release"`
|
||||
3) Push branch & tag to github!
|
||||
2) Packages from master a built automatically by circle CI for this repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
|
||||
|
||||
### Non master branch
|
||||
|
||||
When building from non master branch create a new branch in repo [grafana/grafana-packer](https://github.com/grafana/grafana-packer)
|
||||
and configure circle.yml to deploy that branch as well, https://github.com/grafana/grafana-packer/blob/master/circle.yml#L25,
|
||||
you also need to update https://github.com/grafana/grafana-packer/blob/v3.1.x/deploy.sh#L7.
|
||||
|
||||
### Windows build
|
||||
|
||||
Sign into ci.appveyor.com and the Grafana project's build history page. Builds for windows take a long time (around 20min)
|
||||
and fail quite often for random reasons so I usually continue with the release process without a windows build already built.
|
||||
|
||||
1) Click on the green build that has the correct version and tag
|
||||
2) Click on `DEPLOYMENTS`
|
||||
3) Click on `NEW DEPLOYMENT`
|
||||
4) Select GrafanaBuildS3
|
||||
4) Select the build you want to deploy.
|
||||
|
||||
The deployment should be quick (just uploads the release zip file to S3)
|
||||
|
||||
|
||||
@@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
datasourceRoute.Get("/", Wrap(GetDataSources))
|
||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
|
||||
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
||||
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
|
||||
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
|
||||
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
||||
datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
|
||||
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
|
||||
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
|
||||
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
|
||||
|
||||
apiRoute.Get("/plugins", Wrap(GetPluginList))
|
||||
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
|
||||
@@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
||||
}, reqOrgAdmin)
|
||||
|
||||
apiRoute.Get("/frontend/settings/", GetFrontendSettings)
|
||||
apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings)
|
||||
apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
@@ -14,6 +15,20 @@ import (
|
||||
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
||||
|
||||
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
|
||||
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
|
||||
User: c.SignedInUser,
|
||||
}
|
||||
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
|
||||
if err != bus.ErrHandlerNotFound {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
permissionType, exists := userPermissionsQuery.Result[id]
|
||||
if exists && permissionType != m.DsPermissionQuery {
|
||||
return nil, errors.New("User not allowed to access datasource")
|
||||
}
|
||||
}
|
||||
|
||||
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
||||
cacheKey := fmt.Sprintf("ds-%d", id)
|
||||
|
||||
@@ -38,7 +53,9 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
|
||||
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||
|
||||
ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
|
||||
dsId := c.ParamsInt64(":id")
|
||||
ds, err := hs.getDatasourceFromCache(dsId, c)
|
||||
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||
return
|
||||
|
||||
@@ -20,8 +20,8 @@ func GetDataSources(c *m.ReqContext) Response {
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
for _, ds := range query.Result {
|
||||
dsItem := dtos.DataSourceListItemDTO{
|
||||
Id: ds.Id,
|
||||
OrgId: ds.OrgId,
|
||||
Id: ds.Id,
|
||||
Name: ds.Name,
|
||||
Url: ds.Url,
|
||||
Type: ds.Type,
|
||||
@@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response {
|
||||
return JSON(200, &result)
|
||||
}
|
||||
|
||||
func GetDataSourceByID(c *m.ReqContext) Response {
|
||||
func GetDataSourceById(c *m.ReqContext) Response {
|
||||
query := m.GetDataSourceByIdQuery{
|
||||
Id: c.ParamsInt64(":id"),
|
||||
OrgId: c.OrgId,
|
||||
@@ -68,14 +68,14 @@ func GetDataSourceByID(c *m.ReqContext) Response {
|
||||
return JSON(200, &dtos)
|
||||
}
|
||||
|
||||
func DeleteDataSourceByID(c *m.ReqContext) Response {
|
||||
func DeleteDataSourceById(c *m.ReqContext) Response {
|
||||
id := c.ParamsInt64(":id")
|
||||
|
||||
if id <= 0 {
|
||||
return Error(400, "Missing valid datasource id", nil)
|
||||
}
|
||||
|
||||
ds, err := getRawDataSourceByID(id, c.OrgId)
|
||||
ds, err := getRawDataSourceById(id, c.OrgId)
|
||||
if err != nil {
|
||||
return Error(400, "Failed to delete datasource", nil)
|
||||
}
|
||||
@@ -186,7 +186,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
|
||||
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
|
||||
func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) {
|
||||
query := m.GetDataSourceByIdQuery{
|
||||
Id: id,
|
||||
OrgId: orgID,
|
||||
@@ -236,7 +236,7 @@ func GetDataSourceByName(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
// Get /api/datasources/id/:name
|
||||
func GetDataSourceIDByName(c *m.ReqContext) Response {
|
||||
func GetDataSourceIdByName(c *m.ReqContext) Response {
|
||||
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
|
||||
|
||||
if err := bus.Dispatch(&query); err != nil {
|
||||
|
||||
@@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string {
|
||||
|
||||
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
||||
return &AlertNotification{
|
||||
Id: notification.Id,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Created: notification.Created,
|
||||
Updated: notification.Updated,
|
||||
Frequency: formatShort(notification.Frequency),
|
||||
SendReminder: notification.SendReminder,
|
||||
Settings: notification.Settings,
|
||||
Id: notification.Id,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Created: notification.Created,
|
||||
Updated: notification.Updated,
|
||||
Frequency: formatShort(notification.Frequency),
|
||||
SendReminder: notification.SendReminder,
|
||||
DisableResolveMessage: notification.DisableResolveMessage,
|
||||
Settings: notification.Settings,
|
||||
}
|
||||
}
|
||||
|
||||
type AlertNotification struct {
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Id int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Frequency string `json:"frequency"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
}
|
||||
|
||||
type AlertTestCommand struct {
|
||||
@@ -100,11 +102,12 @@ type EvalMatch struct {
|
||||
}
|
||||
|
||||
type NotificationTestCommand struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Frequency string `json:"frequency"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
}
|
||||
|
||||
type PauseAlertCommand struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
||||
func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
||||
orgDataSources := make([]*m.DataSource, 0)
|
||||
|
||||
if c.OrgId != 0 {
|
||||
@@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgDataSources = query.Result
|
||||
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
|
||||
User: c.SignedInUser,
|
||||
Datasources: query.Result,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&dsFilterQuery); err != nil {
|
||||
if err != bus.ErrHandlerNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
orgDataSources = query.Result
|
||||
} else {
|
||||
orgDataSources = dsFilterQuery.Result
|
||||
}
|
||||
}
|
||||
|
||||
datasources := make(map[string]interface{})
|
||||
@@ -120,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
||||
|
||||
panels := map[string]interface{}{}
|
||||
for _, panel := range enabledPlugins.Panels {
|
||||
if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
|
||||
continue
|
||||
}
|
||||
|
||||
panels[panel.Id] = map[string]interface{}{
|
||||
"module": panel.Module,
|
||||
"baseUrl": panel.BaseUrl,
|
||||
@@ -183,8 +200,8 @@ func getPanelSort(id string) int {
|
||||
return sort
|
||||
}
|
||||
|
||||
func GetFrontendSettings(c *m.ReqContext) {
|
||||
settings, err := getFrontendSettingsMap(c)
|
||||
func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
|
||||
settings, err := hs.getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
c.JsonApiErr(400, "Failed to get frontend settings", err)
|
||||
return
|
||||
|
||||
@@ -18,7 +18,7 @@ const (
|
||||
)
|
||||
|
||||
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
settings, err := getFrontendSettingsMap(c)
|
||||
settings, err := hs.getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
//ApplyRoute should use the plugin route data to set auth headers and custom headers
|
||||
@@ -54,15 +55,30 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route
|
||||
}
|
||||
}
|
||||
|
||||
if route.JwtTokenAuth != nil {
|
||||
authenticationType := ds.JsonData.Get("authenticationType").MustString("jwt")
|
||||
if route.JwtTokenAuth != nil && authenticationType == "jwt" {
|
||||
if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil {
|
||||
logger.Error("Failed to get access token", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
|
||||
}
|
||||
}
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
|
||||
if authenticationType == "gce" {
|
||||
tokenSrc, err := google.DefaultTokenSource(ctx, route.JwtTokenAuth.Scopes...)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get default token from meta data server", "error", err)
|
||||
} else {
|
||||
token, err := tokenSrc.Token()
|
||||
if err != nil {
|
||||
logger.Error("Failed to get default access token from meta data server", "error", err)
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("Requesting", "url", req.URL.String())
|
||||
}
|
||||
|
||||
func interpolateString(text string, data templateData) (string, error) {
|
||||
|
||||
@@ -3,6 +3,8 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
@@ -11,16 +13,12 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
_ "net/http/pprof"
|
||||
|
||||
extensions "github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
extensions "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/elasticsearch"
|
||||
_ "github.com/grafana/grafana/pkg/tsdb/graphite"
|
||||
@@ -35,6 +33,7 @@ import (
|
||||
|
||||
var version = "5.0.0"
|
||||
var commit = "NA"
|
||||
var buildBranch = "master"
|
||||
var buildstamp string
|
||||
|
||||
var configFile = flag.String("config", "", "path to config file")
|
||||
@@ -47,7 +46,7 @@ func main() {
|
||||
profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling")
|
||||
flag.Parse()
|
||||
if *v {
|
||||
fmt.Printf("Version %s (commit: %s)\n", version, commit)
|
||||
fmt.Printf("Version %s (commit: %s, branch: %s)\n", version, commit, buildBranch)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
@@ -78,6 +77,7 @@ func main() {
|
||||
setting.BuildVersion = version
|
||||
setting.BuildCommit = commit
|
||||
setting.BuildStamp = buildstampInt64
|
||||
setting.BuildBranch = buildBranch
|
||||
setting.IsEnterprise = extensions.IsEnterprise
|
||||
|
||||
metrics.M_Grafana_Version.WithLabelValues(version).Set(1)
|
||||
|
||||
@@ -12,24 +12,16 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/facebookgo/inject"
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api"
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/login"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
"github.com/grafana/grafana/pkg/social"
|
||||
|
||||
// self registering services
|
||||
_ "github.com/grafana/grafana/pkg/extensions"
|
||||
_ "github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
_ "github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
_ "github.com/grafana/grafana/pkg/services/alerting"
|
||||
_ "github.com/grafana/grafana/pkg/services/cleanup"
|
||||
_ "github.com/grafana/grafana/pkg/services/notifications"
|
||||
@@ -37,7 +29,10 @@ import (
|
||||
_ "github.com/grafana/grafana/pkg/services/rendering"
|
||||
_ "github.com/grafana/grafana/pkg/services/search"
|
||||
_ "github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/social" // self registering services
|
||||
_ "github.com/grafana/grafana/pkg/tracing"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
func NewGrafanaServer() *GrafanaServerImpl {
|
||||
@@ -159,7 +154,7 @@ func (g *GrafanaServerImpl) loadConfiguration() {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "branch", buildBranch, "compiled", time.Unix(setting.BuildStamp, 0))
|
||||
g.cfg.LogConfigSources()
|
||||
}
|
||||
|
||||
|
||||
@@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
||||
|
||||
if ldapUser.isMemberOf(group.GroupDN) {
|
||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,12 +43,13 @@ func GetContextHandler() macaron.Handler {
|
||||
// then init session and look for userId in session
|
||||
// then look for api key in session (special case for render calls via api)
|
||||
// then test if anonymous access is enabled
|
||||
if initContextWithRenderAuth(ctx) ||
|
||||
initContextWithApiKey(ctx) ||
|
||||
initContextWithBasicAuth(ctx, orgId) ||
|
||||
initContextWithAuthProxy(ctx, orgId) ||
|
||||
initContextWithUserSessionCookie(ctx, orgId) ||
|
||||
initContextWithAnonymousUser(ctx) {
|
||||
switch {
|
||||
case initContextWithRenderAuth(ctx):
|
||||
case initContextWithApiKey(ctx):
|
||||
case initContextWithBasicAuth(ctx, orgId):
|
||||
case initContextWithAuthProxy(ctx, orgId):
|
||||
case initContextWithUserSessionCookie(ctx, orgId):
|
||||
case initContextWithAnonymousUser(ctx):
|
||||
}
|
||||
|
||||
ctx.Logger = log.New("context", "userId", ctx.UserId, "orgId", ctx.OrgId, "uname", ctx.Login)
|
||||
|
||||
@@ -23,38 +23,41 @@ var (
|
||||
)
|
||||
|
||||
type AlertNotification struct {
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency time.Duration `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
Id int64 `json:"id"`
|
||||
OrgId int64 `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Frequency time.Duration `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Created time.Time `json:"created"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
|
||||
type CreateAlertNotificationCommand struct {
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
Result *AlertNotification
|
||||
}
|
||||
|
||||
type UpdateAlertNotificationCommand struct {
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||
Id int64 `json:"id" binding:"Required"`
|
||||
Name string `json:"name" binding:"Required"`
|
||||
Type string `json:"type" binding:"Required"`
|
||||
SendReminder bool `json:"sendReminder"`
|
||||
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||
Frequency string `json:"frequency"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||
|
||||
OrgId int64 `json:"-"`
|
||||
Result *AlertNotification
|
||||
|
||||
@@ -30,6 +30,7 @@ var (
|
||||
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
||||
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
||||
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
|
||||
ErrDataSourceAccessDenied = errors.New("Data source access denied")
|
||||
)
|
||||
|
||||
type DsAccess string
|
||||
@@ -167,6 +168,7 @@ type DeleteDataSourceByNameCommand struct {
|
||||
|
||||
type GetDataSourcesQuery struct {
|
||||
OrgId int64
|
||||
User *SignedInUser
|
||||
Result []*DataSource
|
||||
}
|
||||
|
||||
@@ -187,6 +189,31 @@ type GetDataSourceByNameQuery struct {
|
||||
}
|
||||
|
||||
// ---------------------
|
||||
// EVENTS
|
||||
type DataSourceCreatedEvent struct {
|
||||
// Permissions
|
||||
// ---------------------
|
||||
|
||||
type DsPermissionType int
|
||||
|
||||
const (
|
||||
DsPermissionNoAccess DsPermissionType = iota
|
||||
DsPermissionQuery
|
||||
)
|
||||
|
||||
func (p DsPermissionType) String() string {
|
||||
names := map[int]string{
|
||||
int(DsPermissionQuery): "Query",
|
||||
int(DsPermissionNoAccess): "No Access",
|
||||
}
|
||||
return names[int(p)]
|
||||
}
|
||||
|
||||
type GetDataSourcePermissionsForUserQuery struct {
|
||||
User *SignedInUser
|
||||
Result map[int64]DsPermissionType
|
||||
}
|
||||
|
||||
type DatasourcesPermissionFilterQuery struct {
|
||||
User *SignedInUser
|
||||
Datasources []*DataSource
|
||||
Result []*DataSource
|
||||
}
|
||||
|
||||
@@ -121,7 +121,6 @@ func (pm *PluginManager) Run(ctx context.Context) error {
|
||||
pm.checkForUpdates()
|
||||
case <-ctx.Done():
|
||||
run = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ type Notifier interface {
|
||||
GetNotifierId() int64
|
||||
GetIsDefault() bool
|
||||
GetSendReminder() bool
|
||||
GetDisableResolveMessage() bool
|
||||
GetFrequency() time.Duration
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
@@ -15,13 +14,14 @@ const (
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
SendReminder bool
|
||||
Frequency time.Duration
|
||||
Name string
|
||||
Type string
|
||||
Id int64
|
||||
IsDeault bool
|
||||
UploadImage bool
|
||||
SendReminder bool
|
||||
DisableResolveMessage bool
|
||||
Frequency time.Duration
|
||||
|
||||
log log.Logger
|
||||
}
|
||||
@@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
|
||||
}
|
||||
|
||||
return NotifierBase{
|
||||
Id: model.Id,
|
||||
Name: model.Name,
|
||||
IsDeault: model.IsDefault,
|
||||
Type: model.Type,
|
||||
UploadImage: uploadImage,
|
||||
SendReminder: model.SendReminder,
|
||||
Frequency: model.Frequency,
|
||||
log: log.New("alerting.notifier." + model.Name),
|
||||
Id: model.Id,
|
||||
Name: model.Name,
|
||||
IsDeault: model.IsDefault,
|
||||
Type: model.Type,
|
||||
UploadImage: uploadImage,
|
||||
SendReminder: model.SendReminder,
|
||||
DisableResolveMessage: model.DisableResolveMessage,
|
||||
Frequency: model.Frequency,
|
||||
log: log.New("alerting.notifier." + model.Name),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +84,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC
|
||||
}
|
||||
}
|
||||
|
||||
// Do not notify when state is OK if DisableResolveMessage is set to true
|
||||
if context.Rule.State == models.AlertStateOK && n.DisableResolveMessage {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool {
|
||||
return n.SendReminder
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetDisableResolveMessage() bool {
|
||||
return n.DisableResolveMessage
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetFrequency() time.Duration {
|
||||
return n.Frequency
|
||||
}
|
||||
|
||||
@@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) {
|
||||
base := NewNotifierBase(model)
|
||||
So(base.UploadImage, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("default value should be false for backwards compatibility", func() {
|
||||
base := NewNotifierBase(model)
|
||||
So(base.DisableResolveMessage, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -57,6 +57,9 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
|
||||
message := evalContext.Rule.Message
|
||||
picUrl := evalContext.ImagePublicUrl
|
||||
title := evalContext.GetNotificationTitle()
|
||||
if message == "" {
|
||||
message = title
|
||||
}
|
||||
|
||||
bodyJSON, err := simplejson.NewJson([]byte(`{
|
||||
"msgtype": "link",
|
||||
|
||||
@@ -34,11 +34,8 @@ func NewRuleReader() *DefaultRuleReader {
|
||||
func (arr *DefaultRuleReader) initReader() {
|
||||
heartbeat := time.NewTicker(time.Second * 10)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-heartbeat.C:
|
||||
arr.heartbeat()
|
||||
}
|
||||
for range heartbeat.C {
|
||||
arr.heartbeat()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ var New = func(dashId int64, orgId int64, user *m.SignedInUser) DashboardGuardia
|
||||
user: user,
|
||||
dashId: dashId,
|
||||
orgId: orgId,
|
||||
log: log.New("guardians.dashboard"),
|
||||
log: log.New("dashboard.permissions"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,15 +66,30 @@ func (g *dashboardGuardianImpl) CanAdmin() (bool, error) {
|
||||
|
||||
func (g *dashboardGuardianImpl) HasPermission(permission m.PermissionType) (bool, error) {
|
||||
if g.user.OrgRole == m.ROLE_ADMIN {
|
||||
return true, nil
|
||||
return g.logHasPermissionResult(permission, true, nil)
|
||||
}
|
||||
|
||||
acl, err := g.GetAcl()
|
||||
if err != nil {
|
||||
return false, err
|
||||
return g.logHasPermissionResult(permission, false, err)
|
||||
}
|
||||
|
||||
return g.checkAcl(permission, acl)
|
||||
result, err := g.checkAcl(permission, acl)
|
||||
return g.logHasPermissionResult(permission, result, err)
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) logHasPermissionResult(permission m.PermissionType, hasPermission bool, err error) (bool, error) {
|
||||
if err != nil {
|
||||
return hasPermission, err
|
||||
}
|
||||
|
||||
if hasPermission {
|
||||
g.log.Debug("User granted access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
|
||||
} else {
|
||||
g.log.Debug("User denied access to execute action", "userId", g.user.UserId, "orgId", g.orgId, "uname", g.user.Login, "dashId", g.dashId, "action", permission)
|
||||
}
|
||||
|
||||
return hasPermission, err
|
||||
}
|
||||
|
||||
func (g *dashboardGuardianImpl) checkAcl(permission m.PermissionType, acl []*m.DashboardAclInfoDTO) (bool, error) {
|
||||
|
||||
@@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
|
||||
alert_notification.updated,
|
||||
alert_notification.settings,
|
||||
alert_notification.is_default,
|
||||
alert_notification.disable_resolve_message,
|
||||
alert_notification.send_reminder,
|
||||
alert_notification.frequency
|
||||
FROM alert_notification
|
||||
@@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
|
||||
alert_notification.updated,
|
||||
alert_notification.settings,
|
||||
alert_notification.is_default,
|
||||
alert_notification.disable_resolve_message,
|
||||
alert_notification.send_reminder,
|
||||
alert_notification.frequency
|
||||
FROM alert_notification
|
||||
@@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
||||
}
|
||||
|
||||
alertNotification := &m.AlertNotification{
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
SendReminder: cmd.SendReminder,
|
||||
Frequency: frequency,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDefault: cmd.IsDefault,
|
||||
OrgId: cmd.OrgId,
|
||||
Name: cmd.Name,
|
||||
Type: cmd.Type,
|
||||
Settings: cmd.Settings,
|
||||
SendReminder: cmd.SendReminder,
|
||||
DisableResolveMessage: cmd.DisableResolveMessage,
|
||||
Frequency: frequency,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
IsDefault: cmd.IsDefault,
|
||||
}
|
||||
|
||||
if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
|
||||
@@ -210,6 +213,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||
current.Type = cmd.Type
|
||||
current.IsDefault = cmd.IsDefault
|
||||
current.SendReminder = cmd.SendReminder
|
||||
current.DisableResolveMessage = cmd.DisableResolveMessage
|
||||
|
||||
if current.SendReminder {
|
||||
if cmd.Frequency == "" {
|
||||
@@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||
current.Frequency = frequency
|
||||
}
|
||||
|
||||
sess.UseBool("is_default", "send_reminder")
|
||||
sess.UseBool("is_default", "send_reminder", "disable_resolve_message")
|
||||
|
||||
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
|
||||
return err
|
||||
|
||||
@@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
So(cmd.Result.OrgId, ShouldNotEqual, 0)
|
||||
So(cmd.Result.Type, ShouldEqual, "email")
|
||||
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
||||
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
|
||||
|
||||
Convey("Cannot save Alert Notification with the same name", func() {
|
||||
err = CreateAlertNotificationCommand(cmd)
|
||||
@@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
||||
|
||||
Convey("Can update alert notification", func() {
|
||||
newCmd := &models.UpdateAlertNotificationCommand{
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
SendReminder: true,
|
||||
Frequency: "60s",
|
||||
Settings: simplejson.New(),
|
||||
Id: cmd.Result.Id,
|
||||
Name: "NewName",
|
||||
Type: "webhook",
|
||||
OrgId: cmd.Result.OrgId,
|
||||
SendReminder: true,
|
||||
DisableResolveMessage: true,
|
||||
Frequency: "60s",
|
||||
Settings: simplejson.New(),
|
||||
Id: cmd.Result.Id,
|
||||
}
|
||||
err := UpdateAlertNotification(newCmd)
|
||||
So(err, ShouldBeNil)
|
||||
So(newCmd.Result.Name, ShouldEqual, "NewName")
|
||||
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
|
||||
So(newCmd.Result.DisableResolveMessage, ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("Can update alert notification to disable sending of reminders", func() {
|
||||
|
||||
@@ -320,13 +320,18 @@ func DeleteDashboard(cmd *m.DeleteDashboardCommand) error {
|
||||
"DELETE FROM dashboard WHERE id = ?",
|
||||
"DELETE FROM playlist_item WHERE type = 'dashboard_by_id' AND value = ?",
|
||||
"DELETE FROM dashboard_version WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard WHERE folder_id = ?",
|
||||
"DELETE FROM annotation WHERE dashboard_id = ?",
|
||||
"DELETE FROM dashboard_provisioning WHERE dashboard_id = ?",
|
||||
}
|
||||
|
||||
if dashboard.IsFolder {
|
||||
deletes = append(deletes, "DELETE FROM dashboard_provisioning WHERE dashboard_id in (select id from dashboard where folder_id = ?)")
|
||||
deletes = append(deletes, "DELETE FROM dashboard WHERE folder_id = ?")
|
||||
}
|
||||
|
||||
for _, sql := range deletes {
|
||||
_, err := sess.Exec(sql, dashboard.Id)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -13,17 +13,30 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
Convey("Testing Dashboard provisioning", t, func() {
|
||||
InitTestDB(t)
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
folderCmd := &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
FolderId: 0,
|
||||
IsFolder: false,
|
||||
IsFolder: true,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dashboard",
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("Saving dashboards with extras", func() {
|
||||
err := SaveDashboard(folderCmd)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
saveDashboardCmd := &models.SaveDashboardCommand{
|
||||
OrgId: 1,
|
||||
IsFolder: false,
|
||||
FolderId: folderCmd.Result.Id,
|
||||
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||
"id": nil,
|
||||
"title": "test dashboard",
|
||||
}),
|
||||
}
|
||||
|
||||
Convey("Saving dashboards with provisioning meta data", func() {
|
||||
now := time.Now()
|
||||
|
||||
cmd := &models.SaveProvisionedDashboardCommand{
|
||||
@@ -67,6 +80,21 @@ func TestDashboardProvisioningTest(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
|
||||
Convey("Deleteing folder should delete provision meta data", func() {
|
||||
deleteCmd := &models.DeleteDashboardCommand{
|
||||
Id: folderCmd.Result.Id,
|
||||
OrgId: 1,
|
||||
}
|
||||
|
||||
So(DeleteDashboard(deleteCmd), ShouldBeNil)
|
||||
|
||||
query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id}
|
||||
|
||||
err = GetProvisionedDataByDashboardId(query)
|
||||
So(err, ShouldBeNil)
|
||||
So(query.Result, ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
|
||||
|
||||
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
|
||||
has, err := x.Get(&datasource)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) {
|
||||
mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
|
||||
Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
|
||||
}))
|
||||
mg.AddMigration("Add column disable_resolve_message", NewAddColumnMigration(alert_notification, &Column{
|
||||
Name: "disable_resolve_message", Type: DB_Bool, Nullable: false, Default: "0",
|
||||
}))
|
||||
|
||||
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@ type SqlStore struct {
|
||||
dbCfg DatabaseConfig
|
||||
engine *xorm.Engine
|
||||
log log.Logger
|
||||
Dialect migrator.Dialect
|
||||
skipEnsureAdmin bool
|
||||
}
|
||||
|
||||
@@ -125,10 +126,12 @@ func (ss *SqlStore) Init() error {
|
||||
}
|
||||
|
||||
ss.engine = engine
|
||||
ss.Dialect = migrator.NewDialect(ss.engine)
|
||||
|
||||
// temporarily still set global var
|
||||
x = engine
|
||||
dialect = migrator.NewDialect(x)
|
||||
dialect = ss.Dialect
|
||||
|
||||
migrator := migrator.NewMigrator(x)
|
||||
migrations.AddMigrations(migrator)
|
||||
|
||||
@@ -347,7 +350,11 @@ func InitTestDB(t *testing.T) *SqlStore {
|
||||
t.Fatalf("Failed to init test database: %v", err)
|
||||
}
|
||||
|
||||
dialect = migrator.NewDialect(engine)
|
||||
sqlstore.Dialect = migrator.NewDialect(engine)
|
||||
|
||||
// temp global var until we get rid of global vars
|
||||
dialect = sqlstore.Dialect
|
||||
|
||||
if err := dialect.CleanDB(); err != nil {
|
||||
t.Fatalf("Failed to clean test db %v", err)
|
||||
}
|
||||
|
||||
@@ -13,15 +13,12 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/go-macaron/session"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
type Scheme string
|
||||
@@ -49,6 +46,7 @@ var (
|
||||
// build
|
||||
BuildVersion string
|
||||
BuildCommit string
|
||||
BuildBranch string
|
||||
BuildStamp int64
|
||||
IsEnterprise bool
|
||||
ApplicationName string
|
||||
@@ -213,6 +211,8 @@ type Cfg struct {
|
||||
TempDataLifetime time.Duration
|
||||
|
||||
MetricsEndpointEnabled bool
|
||||
|
||||
EnableAlphaPanels bool
|
||||
}
|
||||
|
||||
type CommandLineArgs struct {
|
||||
@@ -694,6 +694,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
||||
explore := iniFile.Section("explore")
|
||||
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
||||
|
||||
panels := iniFile.Section("panels")
|
||||
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
|
||||
|
||||
cfg.readSessionConfig()
|
||||
cfg.readSmtpSettings()
|
||||
cfg.readQuotaSettings()
|
||||
|
||||
@@ -86,9 +86,10 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
result := &tsdb.Response{
|
||||
results := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
|
||||
|
||||
eg, ectx := errgroup.WithContext(ctx)
|
||||
|
||||
@@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
RefId := queryContext.Queries[i].RefId
|
||||
query, err := parseQuery(queryContext.Queries[i].Model)
|
||||
if err != nil {
|
||||
result.Results[RefId] = &tsdb.QueryResult{
|
||||
results.Results[RefId] = &tsdb.QueryResult{
|
||||
Error: err,
|
||||
}
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
query.RefId = RefId
|
||||
|
||||
@@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
}
|
||||
|
||||
if query.Id == "" && query.Expression != "" {
|
||||
result.Results[query.RefId] = &tsdb.QueryResult{
|
||||
results.Results[query.RefId] = &tsdb.QueryResult{
|
||||
Error: fmt.Errorf("Invalid query: id should be set if using expression"),
|
||||
}
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
eg.Go(func() error {
|
||||
@@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
return err
|
||||
}
|
||||
if err != nil {
|
||||
result.Results[query.RefId] = &tsdb.QueryResult{
|
||||
resultChan <- &tsdb.QueryResult{
|
||||
RefId: query.RefId,
|
||||
Error: err,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
resultChan <- queryRes
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
return err
|
||||
}
|
||||
for _, queryRes := range queryResponses {
|
||||
result.Results[queryRes.RefId] = queryRes
|
||||
if err != nil {
|
||||
result.Results[queryRes.RefId].Error = err
|
||||
queryRes.Error = err
|
||||
}
|
||||
resultChan <- queryRes
|
||||
}
|
||||
return nil
|
||||
})
|
||||
@@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
||||
if err := eg.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
close(resultChan)
|
||||
for result := range resultChan {
|
||||
results.Results[result.RefId] = result
|
||||
}
|
||||
|
||||
return result, nil
|
||||
return results, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
|
||||
|
||||
@@ -35,6 +35,7 @@ type CustomMetricsCache struct {
|
||||
|
||||
var customMetricsMetricsMap map[string]map[string]map[string]*CustomMetricsCache
|
||||
var customMetricsDimensionsMap map[string]map[string]map[string]*CustomMetricsCache
|
||||
var regionCache sync.Map
|
||||
|
||||
func init() {
|
||||
metricsMap = map[string][]string{
|
||||
@@ -233,15 +234,50 @@ func parseMultiSelectValue(input string) []string {
|
||||
// Whenever this list is updated, frontend list should also be updated.
|
||||
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
|
||||
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
regions := []string{
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1",
|
||||
"eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1",
|
||||
dsInfo := e.getDsInfo("default")
|
||||
profile := dsInfo.Profile
|
||||
if cache, ok := regionCache.Load(profile); ok {
|
||||
if cache2, ok2 := cache.([]suggestData); ok2 {
|
||||
return cache2, nil
|
||||
}
|
||||
}
|
||||
|
||||
regions := []string{
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1",
|
||||
"eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2",
|
||||
"cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1",
|
||||
}
|
||||
err := e.ensureClientSession("default")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r, err := e.ec2Svc.DescribeRegions(&ec2.DescribeRegionsInput{})
|
||||
if err != nil {
|
||||
// ignore error for backward compatibility
|
||||
plog.Error("Failed to get regions", "error", err)
|
||||
} else {
|
||||
for _, region := range r.Regions {
|
||||
exists := false
|
||||
|
||||
for _, existingRegion := range regions {
|
||||
if existingRegion == *region.RegionName {
|
||||
exists = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !exists {
|
||||
regions = append(regions, *region.RegionName)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(regions)
|
||||
|
||||
result := make([]suggestData, 0)
|
||||
for _, region := range regions {
|
||||
result = append(result, suggestData{Text: region, Value: region})
|
||||
}
|
||||
regionCache.Store(profile, result)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -9,20 +9,26 @@ import (
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
|
||||
"github.com/bmizerany/assert"
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type mockedEc2 struct {
|
||||
ec2iface.EC2API
|
||||
Resp ec2.DescribeInstancesOutput
|
||||
Resp ec2.DescribeInstancesOutput
|
||||
RespRegions ec2.DescribeRegionsOutput
|
||||
}
|
||||
|
||||
func (m mockedEc2) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
|
||||
fn(&m.Resp, true)
|
||||
return nil
|
||||
}
|
||||
func (m mockedEc2) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) {
|
||||
return &m.RespRegions, nil
|
||||
}
|
||||
|
||||
func TestCloudWatchMetrics(t *testing.T) {
|
||||
|
||||
@@ -82,6 +88,31 @@ func TestCloudWatchMetrics(t *testing.T) {
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When calling handleGetRegions", t, func() {
|
||||
executor := &CloudWatchExecutor{
|
||||
ec2Svc: mockedEc2{RespRegions: ec2.DescribeRegionsOutput{
|
||||
Regions: []*ec2.Region{
|
||||
{
|
||||
RegionName: aws.String("ap-northeast-2"),
|
||||
},
|
||||
},
|
||||
}},
|
||||
}
|
||||
jsonData := simplejson.New()
|
||||
jsonData.Set("defaultRegion", "default")
|
||||
executor.DataSource = &models.DataSource{
|
||||
JsonData: jsonData,
|
||||
SecureJsonData: securejsondata.SecureJsonData{},
|
||||
}
|
||||
|
||||
result, _ := executor.handleGetRegions(context.Background(), simplejson.New(), &tsdb.TsdbQuery{})
|
||||
|
||||
Convey("Should return regions", func() {
|
||||
So(result[0].Text, ShouldEqual, "ap-northeast-1")
|
||||
So(result[1].Text, ShouldEqual, "ap-northeast-2")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("When calling handleGetEc2InstanceAttribute", t, func() {
|
||||
executor := &CloudWatchExecutor{
|
||||
ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{
|
||||
|
||||
@@ -164,14 +164,12 @@ func formatTimeRange(input string) string {
|
||||
|
||||
func fixIntervalFormat(target string) string {
|
||||
rMinute := regexp.MustCompile(`'(\d+)m'`)
|
||||
rMin := regexp.MustCompile("m")
|
||||
target = rMinute.ReplaceAllStringFunc(target, func(m string) string {
|
||||
return rMin.ReplaceAllString(m, "min")
|
||||
return strings.Replace(m, "m", "min", -1)
|
||||
})
|
||||
rMonth := regexp.MustCompile(`'(\d+)M'`)
|
||||
rMon := regexp.MustCompile("M")
|
||||
target = rMonth.ReplaceAllStringFunc(target, func(M string) string {
|
||||
return rMon.ReplaceAllString(M, "mon")
|
||||
return strings.Replace(M, "M", "mon", -1)
|
||||
})
|
||||
return target
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er
|
||||
return "", fmt.Errorf("missing time column argument for macro %v", name)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil
|
||||
return fmt.Sprintf("%s BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", args[0], m.timeRange.GetFromAsSecondsEpoch(), m.timeRange.GetToAsSecondsEpoch()), nil
|
||||
case "__timeGroup":
|
||||
if len(args) < 2 {
|
||||
return "", fmt.Errorf("macro %v needs time column and interval", name)
|
||||
|
||||
@@ -60,7 +60,7 @@ func TestMacroEngine(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
@@ -92,7 +92,7 @@ func TestMacroEngine(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
@@ -112,7 +112,7 @@ func TestMacroEngine(t *testing.T) {
|
||||
sql, err := engine.Interpolate(query, timeRange, "WHERE $__timeFilter(time_column)")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN '%s' AND '%s'", from.Format(time.RFC3339), to.Format(time.RFC3339)))
|
||||
So(sql, ShouldEqual, fmt.Sprintf("WHERE time_column BETWEEN FROM_UNIXTIME(%d) AND FROM_UNIXTIME(%d)", from.Unix(), to.Unix()))
|
||||
})
|
||||
|
||||
Convey("interpolate __unixEpochFilter function", func() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/go-sql-driver/mysql"
|
||||
"github.com/go-xorm/core"
|
||||
@@ -20,10 +21,14 @@ func init() {
|
||||
func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
logger := log.New("tsdb.mysql")
|
||||
|
||||
protocol := "tcp"
|
||||
if strings.HasPrefix(datasource.Url, "/") {
|
||||
protocol = "unix"
|
||||
}
|
||||
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
|
||||
datasource.User,
|
||||
datasource.Password,
|
||||
"tcp",
|
||||
protocol,
|
||||
datasource.Url,
|
||||
datasource.Database,
|
||||
)
|
||||
|
||||
24
pkg/tsdb/stackdriver/ensure_default_project.go
Normal file
24
pkg/tsdb/stackdriver/ensure_default_project.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package stackdriver
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func (e *StackdriverExecutor) ensureDefaultProject(ctx context.Context, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: tsdbQuery.Queries[0].RefId}
|
||||
result := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
defaultProject, err := e.getDefaultProject(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
e.dsInfo.JsonData.Set("defaultProject", defaultProject)
|
||||
queryResult.Meta.Set("defaultProject", defaultProject)
|
||||
result.Results[tsdbQuery.Queries[0].RefId] = queryResult
|
||||
return result, nil
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/context/ctxhttp"
|
||||
"golang.org/x/oauth2/google"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
@@ -34,6 +35,11 @@ var (
|
||||
metricNameFormat *regexp.Regexp
|
||||
)
|
||||
|
||||
const (
|
||||
gceAuthentication string = "gce"
|
||||
jwtAuthentication string = "jwt"
|
||||
)
|
||||
|
||||
// StackdriverExecutor executes queries for the Stackdriver datasource
|
||||
type StackdriverExecutor struct {
|
||||
httpClient *http.Client
|
||||
@@ -71,6 +77,8 @@ func (e *StackdriverExecutor) Query(ctx context.Context, dsInfo *models.DataSour
|
||||
switch queryType {
|
||||
case "annotationQuery":
|
||||
result, err = e.executeAnnotationQuery(ctx, tsdbQuery)
|
||||
case "ensureDefaultProjectQuery":
|
||||
result, err = e.ensureDefaultProject(ctx, tsdbQuery)
|
||||
case "timeSeriesQuery":
|
||||
fallthrough
|
||||
default:
|
||||
@@ -85,6 +93,16 @@ func (e *StackdriverExecutor) executeTimeSeriesQuery(ctx context.Context, tsdbQu
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
|
||||
authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
|
||||
if authenticationType == gceAuthentication {
|
||||
defaultProject, err := e.getDefaultProject(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
|
||||
}
|
||||
|
||||
e.dsInfo.JsonData.Set("defaultProject", defaultProject)
|
||||
}
|
||||
|
||||
queries, err := e.buildQueries(tsdbQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -168,8 +186,7 @@ func reverse(s string) string {
|
||||
}
|
||||
|
||||
func interpolateFilterWildcards(value string) string {
|
||||
re := regexp.MustCompile("[*]")
|
||||
matches := len(re.FindAllStringIndex(value, -1))
|
||||
matches := strings.Count(value, "*")
|
||||
if matches == 2 && strings.HasSuffix(value, "*") && strings.HasPrefix(value, "*") {
|
||||
value = strings.Replace(value, "*", "", -1)
|
||||
value = fmt.Sprintf(`has_substring("%s")`, value)
|
||||
@@ -337,11 +354,21 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver
|
||||
func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data StackdriverResponse, query *StackdriverQuery) error {
|
||||
metricLabels := make(map[string][]string)
|
||||
resourceLabels := make(map[string][]string)
|
||||
var resourceTypes []string
|
||||
|
||||
for _, series := range data.TimeSeries {
|
||||
if !containsLabel(resourceTypes, series.Resource.Type) {
|
||||
resourceTypes = append(resourceTypes, series.Resource.Type)
|
||||
}
|
||||
}
|
||||
|
||||
for _, series := range data.TimeSeries {
|
||||
points := make([]tsdb.TimePoint, 0)
|
||||
|
||||
defaultMetricName := series.Metric.Type
|
||||
if len(resourceTypes) > 1 {
|
||||
defaultMetricName += " " + series.Resource.Type
|
||||
}
|
||||
|
||||
for key, value := range series.Metric.Labels {
|
||||
if !containsLabel(metricLabels[key], value) {
|
||||
@@ -385,7 +412,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
points = append(points, tsdb.NewTimePoint(null.FloatFrom(value), float64((point.Interval.EndTime).Unix())*1000))
|
||||
}
|
||||
|
||||
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
|
||||
metricName := formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, make(map[string]string), query)
|
||||
|
||||
queryRes.Series = append(queryRes.Series, &tsdb.TimeSeries{
|
||||
Name: metricName,
|
||||
@@ -411,7 +438,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||
buckets[i] = &tsdb.TimeSeries{
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Points: make([]tsdb.TimePoint, 0),
|
||||
}
|
||||
if maxKey < i {
|
||||
@@ -427,7 +454,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
bucketBound := calcBucketBound(point.Value.DistributionValue.BucketOptions, i)
|
||||
additionalLabels := map[string]string{"bucket": bucketBound}
|
||||
buckets[i] = &tsdb.TimeSeries{
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Name: formatLegendKeys(series.Metric.Type, defaultMetricName, series.Resource.Type, series.Metric.Labels, series.Resource.Labels, additionalLabels, query),
|
||||
Points: make([]tsdb.TimePoint, 0),
|
||||
}
|
||||
}
|
||||
@@ -442,6 +469,7 @@ func (e *StackdriverExecutor) parseResponse(queryRes *tsdb.QueryResult, data Sta
|
||||
queryRes.Meta.Set("resourceLabels", resourceLabels)
|
||||
queryRes.Meta.Set("metricLabels", metricLabels)
|
||||
queryRes.Meta.Set("groupBys", query.GroupBys)
|
||||
queryRes.Meta.Set("resourceTypes", resourceTypes)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -455,7 +483,7 @@ func containsLabel(labels []string, newLabel string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func formatLegendKeys(metricType string, defaultMetricName string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
|
||||
func formatLegendKeys(metricType string, defaultMetricName string, resourceType string, metricLabels map[string]string, resourceLabels map[string]string, additionalLabels map[string]string, query *StackdriverQuery) string {
|
||||
if query.AliasBy == "" {
|
||||
return defaultMetricName
|
||||
}
|
||||
@@ -469,6 +497,10 @@ func formatLegendKeys(metricType string, defaultMetricName string, metricLabels
|
||||
return []byte(metricType)
|
||||
}
|
||||
|
||||
if metaPartName == "resource.type" && resourceType != "" {
|
||||
return []byte(resourceType)
|
||||
}
|
||||
|
||||
metricPart := replaceWithMetricPart(metaPartName, metricType)
|
||||
|
||||
if metricPart != nil {
|
||||
@@ -550,8 +582,6 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
|
||||
if !ok {
|
||||
return nil, errors.New("Unable to find datasource plugin Stackdriver")
|
||||
}
|
||||
projectName := dsInfo.JsonData.Get("defaultProject").MustString()
|
||||
proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
|
||||
|
||||
var stackdriverRoute *plugins.AppPluginRoute
|
||||
for _, route := range plugin.Routes {
|
||||
@@ -561,7 +591,22 @@ func (e *StackdriverExecutor) createRequest(ctx context.Context, dsInfo *models.
|
||||
}
|
||||
}
|
||||
|
||||
projectName := dsInfo.JsonData.Get("defaultProject").MustString()
|
||||
proxyPass := fmt.Sprintf("stackdriver%s", "v3/projects/"+projectName+"/timeSeries")
|
||||
|
||||
pluginproxy.ApplyRoute(ctx, req, proxyPass, stackdriverRoute, dsInfo)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func (e *StackdriverExecutor) getDefaultProject(ctx context.Context) (string, error) {
|
||||
authenticationType := e.dsInfo.JsonData.Get("authenticationType").MustString(jwtAuthentication)
|
||||
if authenticationType == gceAuthentication {
|
||||
defaultCredentials, err := google.FindDefaultCredentials(ctx, "https://www.googleapis.com/auth/monitoring.read")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to retrieve default project from GCE metadata server. error: %v", err)
|
||||
}
|
||||
return defaultCredentials.ProjectID, nil
|
||||
}
|
||||
return e.dsInfo.JsonData.Get("defaultProject").MustString(), nil
|
||||
}
|
||||
|
||||
@@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => {
|
||||
return array;
|
||||
};
|
||||
|
||||
import { coreModule, registerAngularDirectives } from './core/core';
|
||||
import { setupAngularRoutes } from './routes/routes';
|
||||
import { coreModule, angularModules } from 'app/core/core_module';
|
||||
import { registerAngularDirectives } from 'app/core/core';
|
||||
import { setupAngularRoutes } from 'app/routes/routes';
|
||||
|
||||
import 'app/routes/GrafanaCtrl';
|
||||
import 'app/features/all';
|
||||
|
||||
// import symlinked extensions
|
||||
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
||||
@@ -109,39 +113,26 @@ export class GrafanaApp {
|
||||
'react',
|
||||
];
|
||||
|
||||
const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes'];
|
||||
|
||||
_.each(moduleTypes, type => {
|
||||
const moduleName = 'grafana.' + type;
|
||||
this.useModule(angular.module(moduleName, []));
|
||||
});
|
||||
|
||||
// makes it possible to add dynamic stuff
|
||||
this.useModule(coreModule);
|
||||
_.each(angularModules, m => {
|
||||
this.useModule(m);
|
||||
});
|
||||
|
||||
// register react angular wrappers
|
||||
coreModule.config(setupAngularRoutes);
|
||||
registerAngularDirectives();
|
||||
|
||||
const preBootRequires = [import('app/features/all')];
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false;
|
||||
|
||||
Promise.all(preBootRequires)
|
||||
.then(() => {
|
||||
// disable tool tip animation
|
||||
$.fn.tooltip.defaults.animation = false;
|
||||
|
||||
// bootstrap the app
|
||||
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
|
||||
_.each(this.preBootModules, module => {
|
||||
_.extend(module, this.registerFunctions);
|
||||
});
|
||||
|
||||
this.preBootModules = null;
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Application boot failed:', err);
|
||||
// bootstrap the app
|
||||
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
|
||||
_.each(this.preBootModules, module => {
|
||||
_.extend(module, this.registerFunctions);
|
||||
});
|
||||
|
||||
this.preBootModules = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
28
public/app/core/actions/appNotification.ts
Normal file
28
public/app/core/actions/appNotification.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { AppNotification } from 'app/types/';
|
||||
|
||||
export enum ActionTypes {
|
||||
AddAppNotification = 'ADD_APP_NOTIFICATION',
|
||||
ClearAppNotification = 'CLEAR_APP_NOTIFICATION',
|
||||
}
|
||||
|
||||
interface AddAppNotificationAction {
|
||||
type: ActionTypes.AddAppNotification;
|
||||
payload: AppNotification;
|
||||
}
|
||||
|
||||
interface ClearAppNotificationAction {
|
||||
type: ActionTypes.ClearAppNotification;
|
||||
payload: number;
|
||||
}
|
||||
|
||||
export type Action = AddAppNotificationAction | ClearAppNotificationAction;
|
||||
|
||||
export const clearAppNotification = (appNotificationId: number) => ({
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: appNotificationId,
|
||||
});
|
||||
|
||||
export const notifyApp = (appNotification: AppNotification) => ({
|
||||
type: ActionTypes.AddAppNotification,
|
||||
payload: appNotification,
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import { updateLocation } from './location';
|
||||
import { updateNavIndex, UpdateNavIndexAction } from './navModel';
|
||||
import { notifyApp, clearAppNotification } from './appNotification';
|
||||
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction };
|
||||
export { updateLocation, updateNavIndex, UpdateNavIndexAction, notifyApp, clearAppNotification };
|
||||
|
||||
@@ -5,10 +5,12 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
|
||||
import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import { SideMenu } from './components/sidemenu/SideMenu';
|
||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||
react2AngularDirective('sidemenu', SideMenu, []);
|
||||
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||
react2AngularDirective('searchResult', SearchResult, []);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, { Component } from 'react';
|
||||
import { AppNotification } from 'app/types';
|
||||
|
||||
interface Props {
|
||||
appNotification: AppNotification;
|
||||
onClearNotification: (id) => void;
|
||||
}
|
||||
|
||||
export default class AppNotificationItem extends Component<Props> {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return this.props.appNotification.id !== nextProps.appNotification.id;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { appNotification, onClearNotification } = this.props;
|
||||
setTimeout(() => {
|
||||
onClearNotification(appNotification.id);
|
||||
}, appNotification.timeout);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { appNotification, onClearNotification } = this.props;
|
||||
return (
|
||||
<div className={`alert-${appNotification.severity} alert`}>
|
||||
<div className="alert-icon">
|
||||
<i className={appNotification.icon} />
|
||||
</div>
|
||||
<div className="alert-body">
|
||||
<div className="alert-title">{appNotification.title}</div>
|
||||
<div className="alert-text">{appNotification.text}</div>
|
||||
</div>
|
||||
<button type="button" className="alert-close" onClick={() => onClearNotification(appNotification.id)}>
|
||||
<i className="fa fa fa-remove" />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import AppNotificationItem from './AppNotificationItem';
|
||||
import { notifyApp, clearAppNotification } from 'app/core/actions';
|
||||
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
|
||||
import { AppNotification, StoreState } from 'app/types';
|
||||
import {
|
||||
createErrorNotification,
|
||||
createSuccessNotification,
|
||||
createWarningNotification,
|
||||
} from '../../copy/appNotification';
|
||||
|
||||
export interface Props {
|
||||
appNotifications: AppNotification[];
|
||||
notifyApp: typeof notifyApp;
|
||||
clearAppNotification: typeof clearAppNotification;
|
||||
}
|
||||
|
||||
export class AppNotificationList extends PureComponent<Props> {
|
||||
componentDidMount() {
|
||||
const { notifyApp } = this.props;
|
||||
|
||||
appEvents.on('alert-warning', options => notifyApp(createWarningNotification(options[0], options[1])));
|
||||
appEvents.on('alert-success', options => notifyApp(createSuccessNotification(options[0], options[1])));
|
||||
appEvents.on('alert-error', options => notifyApp(createErrorNotification(options[0], options[1])));
|
||||
}
|
||||
|
||||
onClearAppNotification = id => {
|
||||
this.props.clearAppNotification(id);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { appNotifications } = this.props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{appNotifications.map((appNotification, index) => {
|
||||
return (
|
||||
<AppNotificationItem
|
||||
key={`${appNotification.id}-${index}`}
|
||||
appNotification={appNotification}
|
||||
onClearNotification={id => this.onClearAppNotification(id)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: StoreState) => ({
|
||||
appNotifications: state.appNotifications.appNotifications,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
notifyApp,
|
||||
clearAppNotification,
|
||||
};
|
||||
|
||||
export default connectWithStore(AppNotificationList, mapStateToProps, mapDispatchToProps);
|
||||
@@ -19,3 +19,4 @@ export const Label: SFC<Props> = props => {
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -18,6 +18,10 @@ export interface Props {
|
||||
}
|
||||
|
||||
class AddPermissions extends Component<Props, NewDashboardAclItem> {
|
||||
static defaultProps = {
|
||||
showPermissionLevels: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = this.getCleanState();
|
||||
|
||||
@@ -22,10 +22,6 @@ export interface Props {
|
||||
const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
|
||||
|
||||
class DescriptionPicker extends Component<Props, any> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
|
||||
const selectedOption = getSelectedOption(optionsWithDesc, value);
|
||||
|
||||
46
public/app/core/components/Switch/Switch.tsx
Normal file
46
public/app/core/components/Switch/Switch.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
export interface Props {
|
||||
label: string;
|
||||
checked: boolean;
|
||||
labelClass?: string;
|
||||
switchClass?: string;
|
||||
onChange: (event) => any;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
id: any;
|
||||
}
|
||||
|
||||
export class Switch extends PureComponent<Props, State> {
|
||||
state = {
|
||||
id: _.uniqueId(),
|
||||
};
|
||||
|
||||
internalOnChange = event => {
|
||||
event.stopPropagation();
|
||||
this.props.onChange(event);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { labelClass, switchClass, label, checked } = this.props;
|
||||
const labelId = `check-${this.state.id}`;
|
||||
const labelClassName = `gf-form-label ${labelClass} pointer`;
|
||||
const switchClassName = `gf-form-switch ${switchClass}`;
|
||||
|
||||
return (
|
||||
<div className="gf-form">
|
||||
{label && (
|
||||
<label htmlFor={labelId} className={labelClassName}>
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<div className={switchClassName}>
|
||||
<input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
|
||||
<label htmlFor={labelId} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ export class HelpCtrl {
|
||||
{ keys: ['p', 's'], description: 'Open Panel Share Modal' },
|
||||
{ keys: ['p', 'd'], description: 'Duplicate Panel' },
|
||||
{ keys: ['p', 'r'], description: 'Remove Panel' },
|
||||
{ keys: ['p', 'l'], description: 'Toggle panel legend' },
|
||||
],
|
||||
'Time Range': [
|
||||
{ keys: ['t', 'z'], description: 'Zoom out time range' },
|
||||
|
||||
@@ -103,7 +103,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
$scope.$apply(() => {
|
||||
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
|
||||
const dynamicOptions = _.map(result, op => {
|
||||
return op.value;
|
||||
return _.escape(op.value);
|
||||
});
|
||||
callback(dynamicOptions);
|
||||
});
|
||||
@@ -117,6 +117,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: value => {
|
||||
value = _.unescape(value);
|
||||
setTimeout(() => {
|
||||
inputBlur.call($input[0], paramIndex);
|
||||
}, 0);
|
||||
|
||||
@@ -18,6 +18,7 @@ export function geminiScrollbar() {
|
||||
let scrollRoot = elem.parent();
|
||||
const scroller = elem;
|
||||
|
||||
console.log('scroll');
|
||||
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
||||
scrollRoot = scroller;
|
||||
}
|
||||
|
||||
@@ -109,12 +109,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
||||
$scope.$apply(() => {
|
||||
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
|
||||
const dynamicOptions = _.map(result, op => {
|
||||
return op.value;
|
||||
return _.escape(op.value);
|
||||
});
|
||||
|
||||
// add current value to dropdown if it's not in dynamicOptions
|
||||
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
|
||||
dynamicOptions.unshift(part.params[paramIndex]);
|
||||
dynamicOptions.unshift(_.escape(part.params[paramIndex]));
|
||||
}
|
||||
|
||||
callback(dynamicOptions);
|
||||
@@ -129,6 +129,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
||||
minLength: 0,
|
||||
items: 1000,
|
||||
updater: value => {
|
||||
value = _.unescape(value);
|
||||
if (value === part.params[paramIndex]) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
|
||||
export interface BuildInfo {
|
||||
version: string;
|
||||
@@ -9,7 +10,7 @@ export interface BuildInfo {
|
||||
|
||||
export class Settings {
|
||||
datasources: any;
|
||||
panels: any;
|
||||
panels: PanelPlugin[];
|
||||
appSubUrl: string;
|
||||
windowTitlePrefix: string;
|
||||
buildInfo: BuildInfo;
|
||||
|
||||
@@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250;
|
||||
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
||||
|
||||
export const LS_PANEL_COPY_KEY = 'panel-copy';
|
||||
|
||||
export const DASHBOARD_TOOLBAR_HEIGHT = 55;
|
||||
export const DASHBOARD_TOP_PADDING = 20;
|
||||
|
||||
46
public/app/core/copy/appNotification.ts
Normal file
46
public/app/core/copy/appNotification.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types';
|
||||
|
||||
const defaultSuccessNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'fa fa-check',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
};
|
||||
|
||||
const defaultWarningNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'fa fa-exclamation',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
};
|
||||
|
||||
const defaultErrorNotification: AppNotification = {
|
||||
title: '',
|
||||
text: '',
|
||||
severity: AppNotificationSeverity.Error,
|
||||
icon: 'fa fa-exclamation-triangle',
|
||||
timeout: AppNotificationTimeout.Error,
|
||||
};
|
||||
|
||||
export const createSuccessNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultSuccessNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
|
||||
export const createErrorNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultErrorNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
|
||||
export const createWarningNotification = (title: string, text?: string): AppNotification => ({
|
||||
...defaultWarningNotification,
|
||||
title: title,
|
||||
text: text,
|
||||
id: Date.now(),
|
||||
});
|
||||
@@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker';
|
||||
import './services/search_srv';
|
||||
import './services/ng_react';
|
||||
|
||||
import { grafanaAppDirective } from './components/grafana_app';
|
||||
import { searchDirective } from './components/search/search';
|
||||
import { infoPopover } from './components/info_popover';
|
||||
import { navbarDirective } from './components/navbar/navbar';
|
||||
@@ -60,7 +59,6 @@ export {
|
||||
registerAngularDirectives,
|
||||
arrayJoin,
|
||||
coreModule,
|
||||
grafanaAppDirective,
|
||||
navbarDirective,
|
||||
searchDirective,
|
||||
liveSrv,
|
||||
|
||||
@@ -1,2 +1,18 @@
|
||||
import angular from 'angular';
|
||||
export default angular.module('grafana.core', ['ngRoute']);
|
||||
|
||||
const coreModule = angular.module('grafana.core', ['ngRoute']);
|
||||
|
||||
// legacy modules
|
||||
const angularModules = [
|
||||
coreModule,
|
||||
angular.module('grafana.controllers', []),
|
||||
angular.module('grafana.directives', []),
|
||||
angular.module('grafana.factories', []),
|
||||
angular.module('grafana.services', []),
|
||||
angular.module('grafana.filters', []),
|
||||
angular.module('grafana.routes', []),
|
||||
];
|
||||
|
||||
export { angularModules, coreModule };
|
||||
|
||||
export default coreModule;
|
||||
|
||||
@@ -2,16 +2,21 @@ import _ from 'lodash';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function dashClass() {
|
||||
function dashClass($timeout) {
|
||||
return {
|
||||
link: ($scope, elem) => {
|
||||
$scope.onAppEvent('panel-fullscreen-enter', () => {
|
||||
elem.toggleClass('panel-in-fullscreen', true);
|
||||
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
|
||||
console.log('view-mode-changed', panel.fullscreen);
|
||||
if (panel.fullscreen) {
|
||||
elem.addClass('panel-in-fullscreen');
|
||||
} else {
|
||||
$timeout(() => {
|
||||
elem.removeClass('panel-in-fullscreen');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$scope.onAppEvent('panel-fullscreen-exit', () => {
|
||||
elem.toggleClass('panel-in-fullscreen', false);
|
||||
});
|
||||
elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true);
|
||||
|
||||
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
||||
if (newValue) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import $ from 'jquery';
|
||||
import coreModule from '../core_module';
|
||||
|
||||
/** @ngInject */
|
||||
export function metricSegment($compile, $sce) {
|
||||
export function metricSegment($compile, $sce, templateSrv) {
|
||||
const inputTemplate =
|
||||
'<input type="text" data-provide="typeahead" ' +
|
||||
' class="gf-form-input input-medium"' +
|
||||
@@ -41,13 +41,11 @@ export function metricSegment($compile, $sce) {
|
||||
return;
|
||||
}
|
||||
|
||||
value = _.unescape(value);
|
||||
|
||||
$scope.$apply(() => {
|
||||
const selected = _.find($scope.altSegments, { value: value });
|
||||
if (selected) {
|
||||
segment.value = selected.value;
|
||||
segment.html = selected.html || selected.value;
|
||||
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value));
|
||||
segment.fake = false;
|
||||
segment.expandable = selected.expandable;
|
||||
|
||||
@@ -56,7 +54,7 @@ export function metricSegment($compile, $sce) {
|
||||
}
|
||||
} else if (segment.custom !== 'false') {
|
||||
segment.value = value;
|
||||
segment.html = $sce.trustAsHtml(value);
|
||||
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value));
|
||||
segment.expandable = true;
|
||||
segment.fake = false;
|
||||
}
|
||||
@@ -95,7 +93,7 @@ export function metricSegment($compile, $sce) {
|
||||
// add custom values
|
||||
if (segment.custom !== 'false') {
|
||||
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
|
||||
options.unshift(segment.value);
|
||||
options.unshift(_.escape(segment.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +103,7 @@ export function metricSegment($compile, $sce) {
|
||||
};
|
||||
|
||||
$scope.updater = value => {
|
||||
value = _.unescape(value);
|
||||
if (value === segment.value) {
|
||||
clearTimeout(cancelBlur);
|
||||
$input.focus();
|
||||
|
||||
51
public/app/core/reducers/appNotification.test.ts
Normal file
51
public/app/core/reducers/appNotification.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { appNotificationsReducer } from './appNotification';
|
||||
import { ActionTypes } from '../actions/appNotification';
|
||||
import { AppNotificationSeverity, AppNotificationTimeout } from 'app/types/';
|
||||
|
||||
describe('clear alert', () => {
|
||||
it('should filter alert', () => {
|
||||
const id1 = 1540301236048;
|
||||
const id2 = 1540301248293;
|
||||
|
||||
const initialState = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
{
|
||||
id: id2,
|
||||
severity: AppNotificationSeverity.Warning,
|
||||
icon: 'warning',
|
||||
title: 'test2',
|
||||
text: 'test alert fail 2',
|
||||
timeout: AppNotificationTimeout.Warning,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const result = appNotificationsReducer(initialState, {
|
||||
type: ActionTypes.ClearAppNotification,
|
||||
payload: id2,
|
||||
});
|
||||
|
||||
const expectedResult = {
|
||||
appNotifications: [
|
||||
{
|
||||
id: id1,
|
||||
severity: AppNotificationSeverity.Success,
|
||||
icon: 'success',
|
||||
title: 'test',
|
||||
text: 'test alert',
|
||||
timeout: AppNotificationTimeout.Success,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
19
public/app/core/reducers/appNotification.ts
Normal file
19
public/app/core/reducers/appNotification.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { AppNotification, AppNotificationsState } from 'app/types/';
|
||||
import { Action, ActionTypes } from '../actions/appNotification';
|
||||
|
||||
export const initialState: AppNotificationsState = {
|
||||
appNotifications: [] as AppNotification[],
|
||||
};
|
||||
|
||||
export const appNotificationsReducer = (state = initialState, action: Action): AppNotificationsState => {
|
||||
switch (action.type) {
|
||||
case ActionTypes.AddAppNotification:
|
||||
return { ...state, appNotifications: state.appNotifications.concat([action.payload]) };
|
||||
case ActionTypes.ClearAppNotification:
|
||||
return {
|
||||
...state,
|
||||
appNotifications: state.appNotifications.filter(appNotification => appNotification.id !== action.payload),
|
||||
};
|
||||
}
|
||||
return state;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import { navIndexReducer as navIndex } from './navModel';
|
||||
import { locationReducer as location } from './location';
|
||||
import { appNotificationsReducer as appNotifications } from './appNotification';
|
||||
|
||||
export default {
|
||||
navIndex,
|
||||
location,
|
||||
appNotifications,
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Action } from 'app/core/actions/location';
|
||||
import { LocationState } from 'app/types';
|
||||
import { renderUrl } from 'app/core/utils/url';
|
||||
import _ from 'lodash';
|
||||
|
||||
export const initialState: LocationState = {
|
||||
url: '',
|
||||
@@ -12,11 +13,17 @@ export const initialState: LocationState = {
|
||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||
switch (action.type) {
|
||||
case 'UPDATE_LOCATION': {
|
||||
const { path, query, routeParams } = action.payload;
|
||||
const { path, routeParams } = action.payload;
|
||||
let query = action.payload.query || state.query;
|
||||
|
||||
if (action.payload.partial) {
|
||||
query = _.defaults(query, state.query);
|
||||
}
|
||||
|
||||
return {
|
||||
url: renderUrl(path || state.path, query),
|
||||
path: path || state.path,
|
||||
query: query || state.query,
|
||||
query: query,
|
||||
routeParams: routeParams || state.routeParams,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,100 +1,12 @@
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export class AlertSrv {
|
||||
list: any[];
|
||||
constructor() {}
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $timeout, private $rootScope) {
|
||||
this.list = [];
|
||||
}
|
||||
|
||||
init() {
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-error',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'error', 12000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-warning',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'warning', 5000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
this.$rootScope.onAppEvent(
|
||||
'alert-success',
|
||||
(e, alert) => {
|
||||
this.set(alert[0], alert[1], 'success', 3000);
|
||||
},
|
||||
this.$rootScope
|
||||
);
|
||||
|
||||
appEvents.on('alert-warning', options => this.set(options[0], options[1], 'warning', 5000));
|
||||
appEvents.on('alert-success', options => this.set(options[0], options[1], 'success', 3000));
|
||||
appEvents.on('alert-error', options => this.set(options[0], options[1], 'error', 7000));
|
||||
}
|
||||
|
||||
getIconForSeverity(severity) {
|
||||
switch (severity) {
|
||||
case 'success':
|
||||
return 'fa fa-check';
|
||||
case 'error':
|
||||
return 'fa fa-exclamation-triangle';
|
||||
default:
|
||||
return 'fa fa-exclamation';
|
||||
}
|
||||
}
|
||||
|
||||
set(title, text, severity, timeout) {
|
||||
if (_.isObject(text)) {
|
||||
console.log('alert error', text);
|
||||
if (text.statusText) {
|
||||
text = `HTTP Error (${text.status}) ${text.statusText}`;
|
||||
}
|
||||
}
|
||||
|
||||
const newAlert = {
|
||||
title: title || '',
|
||||
text: text || '',
|
||||
severity: severity || 'info',
|
||||
icon: this.getIconForSeverity(severity),
|
||||
};
|
||||
|
||||
const newAlertJson = angular.toJson(newAlert);
|
||||
|
||||
// remove same alert if it already exists
|
||||
_.remove(this.list, value => {
|
||||
return angular.toJson(value) === newAlertJson;
|
||||
});
|
||||
|
||||
this.list.push(newAlert);
|
||||
if (timeout > 0) {
|
||||
this.$timeout(() => {
|
||||
this.list = _.without(this.list, newAlert);
|
||||
}, timeout);
|
||||
}
|
||||
|
||||
if (!this.$rootScope.$$phase) {
|
||||
this.$rootScope.$digest();
|
||||
}
|
||||
|
||||
return newAlert;
|
||||
}
|
||||
|
||||
clear(alert) {
|
||||
this.list = _.without(this.list, alert);
|
||||
}
|
||||
|
||||
clearAll() {
|
||||
this.list = [];
|
||||
set() {
|
||||
console.log('old depricated alert srv being used');
|
||||
}
|
||||
}
|
||||
|
||||
// this is just added to not break old plugins that might be using it
|
||||
coreModule.service('alertSrv', AlertSrv);
|
||||
|
||||
@@ -9,7 +9,7 @@ export class BackendSrv {
|
||||
private noBackendCache: boolean;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $http, private alertSrv, private $q, private $timeout, private contextSrv) {}
|
||||
constructor(private $http, private $q, private $timeout, private contextSrv) {}
|
||||
|
||||
get(url, params?) {
|
||||
return this.request({ method: 'GET', url: url, params: params });
|
||||
@@ -49,14 +49,14 @@ export class BackendSrv {
|
||||
}
|
||||
|
||||
if (err.status === 422) {
|
||||
this.alertSrv.set('Validation failed', data.message, 'warning', 4000);
|
||||
appEvents.emit('alert-warning', ['Validation failed', data.message]);
|
||||
throw data;
|
||||
}
|
||||
|
||||
data.severity = 'error';
|
||||
let severity = 'error';
|
||||
|
||||
if (err.status < 500) {
|
||||
data.severity = 'warning';
|
||||
severity = 'warning';
|
||||
}
|
||||
|
||||
if (data.message) {
|
||||
@@ -66,7 +66,8 @@ export class BackendSrv {
|
||||
description = message;
|
||||
message = 'Error';
|
||||
}
|
||||
this.alertSrv.set(message, description, data.severity, 10000);
|
||||
|
||||
appEvents.emit('alert-' + severity, [message, description]);
|
||||
}
|
||||
|
||||
throw data;
|
||||
@@ -93,7 +94,7 @@ export class BackendSrv {
|
||||
if (options.method !== 'GET') {
|
||||
if (results && results.data.message) {
|
||||
if (options.showSuccessAlert !== false) {
|
||||
this.alertSrv.set(results.data.message, '', 'success', 3000);
|
||||
appEvents.emit('alert-success', [results.data.message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import coreModule from '../core_module';
|
||||
|
||||
class DynamicDirectiveSrv {
|
||||
/** @ngInject */
|
||||
constructor(private $compile, private $rootScope) {}
|
||||
constructor(private $compile) {}
|
||||
|
||||
addDirective(element, name, scope) {
|
||||
const child = angular.element(document.createElement(name));
|
||||
@@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
|
||||
}
|
||||
|
||||
link(scope, elem, attrs, options) {
|
||||
options
|
||||
.directive(scope)
|
||||
.then(directiveInfo => {
|
||||
if (!directiveInfo || !directiveInfo.fn) {
|
||||
elem.empty();
|
||||
return;
|
||||
}
|
||||
const directiveInfo = options.directive(scope);
|
||||
if (!directiveInfo || !directiveInfo.fn) {
|
||||
elem.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!directiveInfo.fn.registered) {
|
||||
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
||||
directiveInfo.fn.registered = true;
|
||||
}
|
||||
if (!directiveInfo.fn.registered) {
|
||||
console.log('register panel tab');
|
||||
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
||||
directiveInfo.fn.registered = true;
|
||||
}
|
||||
|
||||
this.addDirective(elem, directiveInfo.name, scope);
|
||||
})
|
||||
.catch(err => {
|
||||
console.log('Plugin load:', err);
|
||||
this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
|
||||
});
|
||||
this.addDirective(elem, directiveInfo.name, scope);
|
||||
}
|
||||
|
||||
create(options) {
|
||||
|
||||
@@ -148,7 +148,7 @@ export class KeybindingSrv {
|
||||
this.bind('mod+o', () => {
|
||||
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
||||
appEvents.emit('graph-hover-clear');
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
dashboard.startRefresh();
|
||||
});
|
||||
|
||||
this.bind('mod+s', e => {
|
||||
@@ -242,6 +242,18 @@ export class KeybindingSrv {
|
||||
}
|
||||
});
|
||||
|
||||
// toggle panel legend
|
||||
this.bind('p l', () => {
|
||||
if (dashboard.meta.focusPanelId) {
|
||||
const panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId);
|
||||
if (panelInfo.panel.legend) {
|
||||
const panelRef = dashboard.getPanelById(dashboard.meta.focusPanelId);
|
||||
panelRef.legend.show = !panelRef.legend.show;
|
||||
panelRef.refresh();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// collapse all rows
|
||||
this.bind('d shift+c', () => {
|
||||
dashboard.collapseRows();
|
||||
@@ -257,7 +269,7 @@ export class KeybindingSrv {
|
||||
});
|
||||
|
||||
this.bind('d r', () => {
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
dashboard.startRefresh();
|
||||
});
|
||||
|
||||
this.bind('d s', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ describe('backend_srv', () => {
|
||||
return Promise.resolve({});
|
||||
};
|
||||
|
||||
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {});
|
||||
const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {});
|
||||
|
||||
describe('when handling errors', () => {
|
||||
it('should return the http status code', async () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import TableModel from 'app/core/table_model';
|
||||
import TableModel, { mergeTablesIntoModel } from 'app/core/table_model';
|
||||
|
||||
describe('when sorting table desc', () => {
|
||||
let table;
|
||||
@@ -79,3 +79,118 @@ describe('when sorting with nulls', () => {
|
||||
expect(values).toEqual([null, null, 'd', 'c', 'b', 'a', '', '']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeTables', () => {
|
||||
const time = new Date().getTime();
|
||||
|
||||
const singleTable = new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value' }],
|
||||
rows: [[time, 'Label Value 1', 42]],
|
||||
});
|
||||
|
||||
const multipleTablesSameColumns = [
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #A' }],
|
||||
rows: [[time, 'Label Value 1', 'Label Value 2', 42]],
|
||||
}),
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #B' }],
|
||||
rows: [[time, 'Label Value 1', 'Label Value 2', 13]],
|
||||
}),
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }],
|
||||
rows: [[time, 'Label Value 1', 'Label Value 2', 4]],
|
||||
}),
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Label Key 2' }, { text: 'Value #C' }],
|
||||
rows: [[time, 'Label Value 1', 'Label Value 2', 7]],
|
||||
}),
|
||||
];
|
||||
|
||||
const multipleTablesDifferentColumns = [
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #A' }],
|
||||
rows: [[time, 'Label Value 1', 42]],
|
||||
}),
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 2' }, { text: 'Value #B' }],
|
||||
rows: [[time, 'Label Value 2', 13]],
|
||||
}),
|
||||
new TableModel({
|
||||
type: 'table',
|
||||
columns: [{ text: 'Time' }, { text: 'Label Key 1' }, { text: 'Value #C' }],
|
||||
rows: [[time, 'Label Value 3', 7]],
|
||||
}),
|
||||
];
|
||||
|
||||
it('should return the single table as is', () => {
|
||||
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
||||
expect(table.columns.length).toBe(3);
|
||||
expect(table.columns[0].text).toBe('Time');
|
||||
expect(table.columns[1].text).toBe('Label Key 1');
|
||||
expect(table.columns[2].text).toBe('Value');
|
||||
});
|
||||
|
||||
it('should return the union of columns for multiple tables', () => {
|
||||
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns);
|
||||
expect(table.columns.length).toBe(6);
|
||||
expect(table.columns[0].text).toBe('Time');
|
||||
expect(table.columns[1].text).toBe('Label Key 1');
|
||||
expect(table.columns[2].text).toBe('Label Key 2');
|
||||
expect(table.columns[3].text).toBe('Value #A');
|
||||
expect(table.columns[4].text).toBe('Value #B');
|
||||
expect(table.columns[5].text).toBe('Value #C');
|
||||
});
|
||||
|
||||
it('should return 1 row for a single table', () => {
|
||||
const table = mergeTablesIntoModel(new TableModel(), singleTable);
|
||||
expect(table.rows.length).toBe(1);
|
||||
expect(table.rows[0][0]).toBe(time);
|
||||
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||
expect(table.rows[0][2]).toBe(42);
|
||||
});
|
||||
|
||||
it('should return 2 rows for a multiple tables with same column values plus one extra row', () => {
|
||||
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesSameColumns);
|
||||
expect(table.rows.length).toBe(2);
|
||||
expect(table.rows[0][0]).toBe(time);
|
||||
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||
expect(table.rows[0][2]).toBe('Label Value 2');
|
||||
expect(table.rows[0][3]).toBe(42);
|
||||
expect(table.rows[0][4]).toBe(13);
|
||||
expect(table.rows[0][5]).toBe(4);
|
||||
expect(table.rows[1][0]).toBe(time);
|
||||
expect(table.rows[1][1]).toBe('Label Value 1');
|
||||
expect(table.rows[1][2]).toBe('Label Value 2');
|
||||
expect(table.rows[1][3]).toBeUndefined();
|
||||
expect(table.rows[1][4]).toBeUndefined();
|
||||
expect(table.rows[1][5]).toBe(7);
|
||||
});
|
||||
|
||||
it('should return 2 rows for multiple tables with different column values', () => {
|
||||
const table = mergeTablesIntoModel(new TableModel(), ...multipleTablesDifferentColumns);
|
||||
expect(table.rows.length).toBe(2);
|
||||
expect(table.columns.length).toBe(6);
|
||||
|
||||
expect(table.rows[0][0]).toBe(time);
|
||||
expect(table.rows[0][1]).toBe('Label Value 1');
|
||||
expect(table.rows[0][2]).toBe(42);
|
||||
expect(table.rows[0][3]).toBe('Label Value 2');
|
||||
expect(table.rows[0][4]).toBe(13);
|
||||
expect(table.rows[0][5]).toBeUndefined();
|
||||
|
||||
expect(table.rows[1][0]).toBe(time);
|
||||
expect(table.rows[1][1]).toBe('Label Value 3');
|
||||
expect(table.rows[1][2]).toBeUndefined();
|
||||
expect(table.rows[1][3]).toBeUndefined();
|
||||
expect(table.rows[1][4]).toBeUndefined();
|
||||
expect(table.rows[1][5]).toBe(7);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import _ from 'lodash';
|
||||
|
||||
interface Column {
|
||||
text: string;
|
||||
title?: string;
|
||||
@@ -14,11 +16,20 @@ export default class TableModel {
|
||||
type: string;
|
||||
columnMap: any;
|
||||
|
||||
constructor() {
|
||||
constructor(table?: any) {
|
||||
this.columns = [];
|
||||
this.columnMap = {};
|
||||
this.rows = [];
|
||||
this.type = 'table';
|
||||
|
||||
if (table) {
|
||||
if (table.columns) {
|
||||
table.columns.forEach(col => this.addColumn(col));
|
||||
}
|
||||
if (table.rows) {
|
||||
table.rows.forEach(row => this.addRow(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort(options) {
|
||||
@@ -52,3 +63,104 @@ export default class TableModel {
|
||||
this.rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true if both rows have matching non-empty fields as well as matching
|
||||
// indexes where one field is empty and the other is not
|
||||
function areRowsMatching(columns, row, otherRow) {
|
||||
let foundFieldToMatch = false;
|
||||
for (let columnIndex = 0; columnIndex < columns.length; columnIndex++) {
|
||||
if (row[columnIndex] !== undefined && otherRow[columnIndex] !== undefined) {
|
||||
if (row[columnIndex] !== otherRow[columnIndex]) {
|
||||
return false;
|
||||
}
|
||||
} else if (row[columnIndex] === undefined || otherRow[columnIndex] === undefined) {
|
||||
foundFieldToMatch = true;
|
||||
}
|
||||
}
|
||||
return foundFieldToMatch;
|
||||
}
|
||||
|
||||
export function mergeTablesIntoModel(dst?: TableModel, ...tables: TableModel[]): TableModel {
|
||||
const model = dst || new TableModel();
|
||||
|
||||
if (arguments.length === 1) {
|
||||
return model;
|
||||
}
|
||||
|
||||
// Single query returns data columns and rows as is
|
||||
if (arguments.length === 2) {
|
||||
model.columns = [...tables[0].columns];
|
||||
model.rows = [...tables[0].rows];
|
||||
return model;
|
||||
}
|
||||
|
||||
// Track column indexes of union: name -> index
|
||||
const columnNames = {};
|
||||
|
||||
// Union of all non-value columns
|
||||
const columnsUnion = tables.slice().reduce((acc, series) => {
|
||||
series.columns.forEach(col => {
|
||||
const { text } = col;
|
||||
if (columnNames[text] === undefined) {
|
||||
columnNames[text] = acc.length;
|
||||
acc.push(col);
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Map old column index to union index per series, e.g.,
|
||||
// given columnNames {A: 0, B: 1} and
|
||||
// data [{columns: [{ text: 'A' }]}, {columns: [{ text: 'B' }]}] => [[0], [1]]
|
||||
const columnIndexMapper = tables.map(series => series.columns.map(col => columnNames[col.text]));
|
||||
|
||||
// Flatten rows of all series and adjust new column indexes
|
||||
const flattenedRows = tables.reduce((acc, series, seriesIndex) => {
|
||||
const mapper = columnIndexMapper[seriesIndex];
|
||||
series.rows.forEach(row => {
|
||||
const alteredRow = [];
|
||||
// Shifting entries according to index mapper
|
||||
mapper.forEach((to, from) => {
|
||||
alteredRow[to] = row[from];
|
||||
});
|
||||
acc.push(alteredRow);
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// Merge rows that have same values for columns
|
||||
const mergedRows = {};
|
||||
const compactedRows = flattenedRows.reduce((acc, row, rowIndex) => {
|
||||
if (!mergedRows[rowIndex]) {
|
||||
// Look from current row onwards
|
||||
let offset = rowIndex + 1;
|
||||
// More than one row can be merged into current row
|
||||
while (offset < flattenedRows.length) {
|
||||
// Find next row that could be merged
|
||||
const match = _.findIndex(flattenedRows, otherRow => areRowsMatching(columnsUnion, row, otherRow), offset);
|
||||
if (match > -1) {
|
||||
const matchedRow = flattenedRows[match];
|
||||
// Merge values from match into current row if there is a gap in the current row
|
||||
for (let columnIndex = 0; columnIndex < columnsUnion.length; columnIndex++) {
|
||||
if (row[columnIndex] === undefined && matchedRow[columnIndex] !== undefined) {
|
||||
row[columnIndex] = matchedRow[columnIndex];
|
||||
}
|
||||
}
|
||||
// Don't visit this row again
|
||||
mergedRows[match] = matchedRow;
|
||||
// Keep looking for more rows to merge
|
||||
offset = match + 1;
|
||||
} else {
|
||||
// No match found, stop looking
|
||||
break;
|
||||
}
|
||||
}
|
||||
acc.push(row);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
model.columns = columnsUnion;
|
||||
model.rows = compactedRows;
|
||||
return model;
|
||||
}
|
||||
|
||||
11
public/app/core/utils/connectWithReduxStore.tsx
Normal file
11
public/app/core/utils/connectWithReduxStore.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { store } from '../../store/configureStore';
|
||||
|
||||
export function connectWithStore(WrappedComponent, ...args) {
|
||||
const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
|
||||
|
||||
return props => {
|
||||
return <ConnectedWrappedComponent {...props} store={store} />;
|
||||
};
|
||||
}
|
||||
@@ -8,23 +8,17 @@ const DEFAULT_EXPLORE_STATE: ExploreState = {
|
||||
datasourceMissing: false,
|
||||
datasourceName: '',
|
||||
exploreDatasources: [],
|
||||
graphResult: null,
|
||||
graphRange: DEFAULT_RANGE,
|
||||
history: [],
|
||||
latency: 0,
|
||||
loading: false,
|
||||
logsResult: null,
|
||||
queries: [],
|
||||
queryErrors: [],
|
||||
queryHints: [],
|
||||
queryTransactions: [],
|
||||
range: DEFAULT_RANGE,
|
||||
requestOptions: null,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
tableResult: null,
|
||||
};
|
||||
|
||||
describe('state functions', () => {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import _ from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { RawTimeRange } from 'app/types/series';
|
||||
|
||||
import * as dateMath from './datemath';
|
||||
|
||||
const spans = {
|
||||
@@ -129,7 +132,7 @@ export function describeTextRange(expr: any) {
|
||||
return opt;
|
||||
}
|
||||
|
||||
export function describeTimeRange(range) {
|
||||
export function describeTimeRange(range: RawTimeRange): string {
|
||||
const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()];
|
||||
if (option) {
|
||||
return option.display;
|
||||
|
||||
@@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl {
|
||||
defaults: any = {
|
||||
type: 'email',
|
||||
sendReminder: false,
|
||||
disableResolveMessage: false,
|
||||
frequency: '15m',
|
||||
settings: {
|
||||
httpMethod: 'POST',
|
||||
|
||||
@@ -21,21 +21,28 @@
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Send on all alerts"
|
||||
label-class="width-12"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.isDefault"
|
||||
tooltip="Use this notification for all alerts">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Include image"
|
||||
label-class="width-12"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.settings.uploadImage"
|
||||
tooltip="Captures an image and include it in the notification">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Disable Resolve Message"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.disableResolveMessage"
|
||||
tooltip="Disable the resolve message [OK] that is sent when alerting state returns to false">
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="Send reminders"
|
||||
label-class="width-12"
|
||||
label-class="width-14"
|
||||
checked="ctrl.model.sendReminder"
|
||||
tooltip="Send additional notifications for triggered alerts">
|
||||
</gf-form-switch>
|
||||
|
||||
@@ -1,25 +1,32 @@
|
||||
import './editor_ctrl';
|
||||
|
||||
// Libaries
|
||||
import angular from 'angular';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Components
|
||||
import './editor_ctrl';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
// Utils & Services
|
||||
import { makeRegions, dedupAnnotations } from './events_processing';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../dashboard/dashboard_model';
|
||||
|
||||
export class AnnotationsSrv {
|
||||
globalAnnotationsPromise: any;
|
||||
alertStatesPromise: any;
|
||||
datasourcePromises: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
|
||||
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
|
||||
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
|
||||
}
|
||||
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {}
|
||||
|
||||
clearCache() {
|
||||
this.globalAnnotationsPromise = null;
|
||||
this.alertStatesPromise = null;
|
||||
this.datasourcePromises = null;
|
||||
init(dashboard: DashboardModel) {
|
||||
// clear promises on refresh events
|
||||
dashboard.on('refresh', () => {
|
||||
this.globalAnnotationsPromise = null;
|
||||
this.alertStatesPromise = null;
|
||||
this.datasourcePromises = null;
|
||||
});
|
||||
}
|
||||
|
||||
getAnnotations(options) {
|
||||
|
||||
@@ -22,7 +22,6 @@ import './export_data/export_data_modal';
|
||||
import './ad_hoc_filters';
|
||||
import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/PanelLoader';
|
||||
import './dashgrid/RowOptions';
|
||||
import './folder_picker/folder_picker';
|
||||
import './move_to_folder_modal/move_to_folder';
|
||||
|
||||
@@ -1,11 +1,16 @@
|
||||
// Utils
|
||||
import config from 'app/core/config';
|
||||
|
||||
import appEvents from 'app/core/app_events';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { PanelContainer } from './dashgrid/PanelContainer';
|
||||
|
||||
// Services
|
||||
import { AnnotationsSrv } from '../annotations/annotations_srv';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from './dashboard_model';
|
||||
import { PanelModel } from './panel_model';
|
||||
|
||||
export class DashboardCtrl implements PanelContainer {
|
||||
export class DashboardCtrl {
|
||||
dashboard: DashboardModel;
|
||||
dashboardViewState: any;
|
||||
loadedFallbackDashboard: boolean;
|
||||
@@ -22,8 +27,8 @@ export class DashboardCtrl implements PanelContainer {
|
||||
private dashboardSrv,
|
||||
private unsavedChangesSrv,
|
||||
private dashboardViewStateSrv,
|
||||
public playlistSrv,
|
||||
private panelLoader
|
||||
private annotationsSrv: AnnotationsSrv,
|
||||
public playlistSrv
|
||||
) {
|
||||
// temp hack due to way dashboards are loaded
|
||||
// can't use controllerAs on route yet
|
||||
@@ -51,6 +56,7 @@ export class DashboardCtrl implements PanelContainer {
|
||||
// init services
|
||||
this.timeSrv.init(dashboard);
|
||||
this.alertingSrv.init(dashboard, data.alerts);
|
||||
this.annotationsSrv.init(dashboard);
|
||||
|
||||
// template values service needs to initialize completely before
|
||||
// the rest of the dashboard can load
|
||||
@@ -74,7 +80,7 @@ export class DashboardCtrl implements PanelContainer {
|
||||
this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard);
|
||||
this.setWindowTitleAndTheme();
|
||||
|
||||
this.$scope.appEvent('dashboard-initialized', dashboard);
|
||||
appEvents.emit('dashboard-initialized', dashboard);
|
||||
})
|
||||
.catch(this.onInitFailed.bind(this, 'Dashboard init failed', true));
|
||||
}
|
||||
@@ -119,14 +125,6 @@ export class DashboardCtrl implements PanelContainer {
|
||||
return this.dashboard;
|
||||
}
|
||||
|
||||
getPanelLoader() {
|
||||
return this.panelLoader;
|
||||
}
|
||||
|
||||
timezoneChanged() {
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
}
|
||||
|
||||
getPanelContainer() {
|
||||
return this;
|
||||
}
|
||||
@@ -168,10 +166,17 @@ export class DashboardCtrl implements PanelContainer {
|
||||
this.dashboard.removePanel(panel);
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
if (this.dashboard) {
|
||||
this.dashboard.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
init(dashboard) {
|
||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
||||
this.$scope.$on('$destroy', this.onDestroy.bind(this));
|
||||
this.setupDashboard(dashboard);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user