mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' into data-source-settings-to-react
This commit is contained in:
commit
c92813f313
@ -4,6 +4,7 @@ init_cmds = [
|
||||
["./bin/grafana-server", "cfg:app_mode=development"]
|
||||
]
|
||||
watch_all = true
|
||||
follow_symlinks = true
|
||||
watch_dirs = [
|
||||
"$WORKDIR/pkg",
|
||||
"$WORKDIR/public/views",
|
||||
|
@ -170,6 +170,7 @@ jobs:
|
||||
- scripts/*.sh
|
||||
- scripts/publish
|
||||
- scripts/build/release_publisher/release_publisher
|
||||
- scripts/build/publish.sh
|
||||
|
||||
build:
|
||||
docker:
|
||||
@ -237,8 +238,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'
|
||||
@ -253,6 +263,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
|
||||
@ -266,6 +323,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
|
||||
@ -312,7 +382,7 @@ workflows:
|
||||
jobs:
|
||||
- build-all:
|
||||
filters: *filter-only-master
|
||||
- build-enterprise:
|
||||
- build-all-enterprise:
|
||||
filters: *filter-only-master
|
||||
- codespell:
|
||||
filters: *filter-only-master
|
||||
@ -355,13 +425,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:
|
||||
@ -384,6 +456,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
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -69,7 +69,6 @@ debug.test
|
||||
/vendor/**/*.yml
|
||||
/vendor/**/*_test.go
|
||||
/vendor/**/.editorconfig
|
||||
/vendor/**/appengine*
|
||||
*.orig
|
||||
|
||||
/devenv/bulk-dashboards/*.json
|
||||
|
26
CHANGELOG.md
26
CHANGELOG.md
@ -2,20 +2,40 @@
|
||||
|
||||
### 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)
|
||||
|
||||
### Minor
|
||||
|
||||
* **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)
|
||||
* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), 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.1 (unreleased)
|
||||
# 5.3.2 (unreleased)
|
||||
|
||||
* **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)
|
||||
* **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)
|
||||
* **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)
|
||||
|
||||
# 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)
|
||||
* **Stackdriver**: Filter option disappears after removing initial filter [#13607](https://github.com/grafana/grafana/issues/13607)
|
||||
* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino)
|
||||
* **InfluxDB**: Fix for annotation issue that caused text to be shown twice [#13553](https://github.com/grafana/grafana/issues/13553)
|
||||
* **Variables**: Fix nesting variables leads to exception and missing refresh [#13628](https://github.com/grafana/grafana/issues/13628)
|
||||
* **Variables**: Prometheus: Single letter labels are not supported [#13641](https://github.com/grafana/grafana/issues/13641), thx [@olshansky](https://github.com/olshansky)
|
||||
* **Graph**: Fix graph time formatting for Last 24h ranges [#13650](https://github.com/grafana/grafana/issues/13650)
|
||||
* **Playlist**: Fix cannot add dashboards with long names to playlist [#13464](https://github.com/grafana/grafana/issues/13464), thx [@neufeldtech](https://github.com/neufeldtech)
|
||||
* **HTTP API**: Fix /api/org/users so that query and limit querystrings works
|
||||
|
||||
# 5.3.0 (2018-10-10)
|
||||
|
||||
@ -68,7 +88,7 @@
|
||||
* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
|
||||
* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
|
||||
* **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano)
|
||||
* ****: **: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
|
||||
* **CloudWatch**: GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
|
||||
* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
|
||||
* **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon)
|
||||
|
||||
|
32
README.md
32
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.
|
||||
@ -138,5 +138,5 @@ plugin development.
|
||||
|
||||
## License
|
||||
|
||||
Grafana is distributed under Apache 2.0 License.
|
||||
Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE.md).
|
||||
|
||||
|
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.
|
@ -5,7 +5,7 @@ os: Windows Server 2012 R2
|
||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||
|
||||
environment:
|
||||
nodejs_version: "6"
|
||||
nodejs_version: "8"
|
||||
GOPATH: C:\gopath
|
||||
GOVERSION: 1.11
|
||||
|
||||
|
@ -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
|
||||
|
@ -156,9 +156,9 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. |
|
||||
| graphiteVersion | string | Graphite | Graphite version |
|
||||
| timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source |
|
||||
| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) |
|
||||
| timeField | string | Elastic | Which field that should be used as timestamp |
|
||||
| interval | string | Elastic | Index date time format |
|
||||
| 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 |
|
||||
| authType | string | Cloudwatch | Auth provider. keys/credentials/arn |
|
||||
| assumeRoleArn | string | Cloudwatch | ARN of Assume Role |
|
||||
| defaultRegion | string | Cloudwatch | AWS region |
|
||||
@ -166,6 +166,7 @@ Since not all datasources have the same configuration settings we only have the
|
||||
| tsdbVersion | string | OpenTSDB | Version |
|
||||
| tsdbResolution | string | OpenTSDB | Resolution |
|
||||
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
|
||||
| encrypt | string | MSSQL | Connection SSL encryption handling. 'disable', 'false' or 'true' |
|
||||
| postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 |
|
||||
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
|
||||
| maxOpenConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of open connections to the database (Grafana v5.4+) |
|
||||
|
@ -128,7 +128,7 @@ Example json body:
|
||||
|
||||
In DingTalk PC Client:
|
||||
|
||||
1. Click "more" icon on left bottom of the panel.
|
||||
1. Click "more" icon on upper right of the panel.
|
||||
|
||||
2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage".
|
||||
|
||||
|
@ -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": "*"
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ Name | Description
|
||||
*Database* | Name of your MSSQL database.
|
||||
*User* | Database user's login/username
|
||||
*Password* | Database user's password
|
||||
*Encrypt* | This option determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server, default `false` (Grafana v5.4+).
|
||||
*Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+).
|
||||
*Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+).
|
||||
*Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours (Grafana v5.4+).
|
||||
@ -72,8 +73,8 @@ Make sure the user does not get any unwanted privileges from the public role.
|
||||
|
||||
### Known Issues
|
||||
|
||||
MSSQL 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled. Due to this you may receive an `Login error: EOF` error when trying to create your datasource.
|
||||
To fix MSSQL 2008 R2 issue, install MSSQL 2008 R2 Service Pack 2. To fix MSSQL 2008 issue, install Microsoft MSSQL 2008 Service Pack 3 and Cumulative update package 3 for MSSQL 2008 SP3.
|
||||
If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect.
|
||||
If possible, we recommend you to use the latest service pack available for optimal compatibility.
|
||||
|
||||
## Query Editor
|
||||
|
||||
|
@ -206,6 +206,7 @@ datasources:
|
||||
jsonData:
|
||||
tokenUri: https://oauth2.googleapis.com/token
|
||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||
defaultProject: my-project-name
|
||||
secureJsonData:
|
||||
privateKey: |
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
|
@ -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
|
||||
|
@ -10,7 +10,7 @@ weight = 1
|
||||
|
||||
# Developer Guide
|
||||
|
||||
You can extend Grafana by writing your own plugins and then share then with other users in [our plugin repository](https://grafana.com/plugins).
|
||||
You can extend Grafana by writing your own plugins and then share them with other users in [our plugin repository](https://grafana.com/plugins).
|
||||
|
||||
## Short version
|
||||
|
||||
@ -33,7 +33,7 @@ There are two blog posts about authoring a plugin that might also be of interest
|
||||
## What languages?
|
||||
|
||||
Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since
|
||||
we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages.
|
||||
we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo if you choose one of those languages.
|
||||
|
||||
## Buildscript
|
||||
|
||||
@ -60,7 +60,6 @@ and [apps]({{< relref "apps.md" >}}) plugins in the documentation.
|
||||
The Grafana SDK is quite small so far and can be found here:
|
||||
|
||||
- [SDK file in Grafana](https://github.com/grafana/grafana/blob/master/public/app/plugins/sdk.ts)
|
||||
- [SDK Readme](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md)
|
||||
|
||||
The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "datasources.md" >}}).
|
||||
|
||||
|
@ -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,4 +1,4 @@
|
||||
{
|
||||
"stable": "5.3.0",
|
||||
"testing": "5.3.0"
|
||||
"stable": "5.3.1",
|
||||
"testing": "5.3.1"
|
||||
}
|
||||
|
@ -80,7 +80,7 @@
|
||||
"style-loader": "^0.21.0",
|
||||
"systemjs": "0.20.19",
|
||||
"systemjs-plugin-css": "^0.1.36",
|
||||
"ts-jest": "^23.1.4",
|
||||
"ts-jest": "^23.10.4",
|
||||
"ts-loader": "^5.1.0",
|
||||
"tslib": "^1.9.3",
|
||||
"tslint": "^5.8.0",
|
||||
@ -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",
|
||||
|
106
pkg/api/api.go
106
pkg/api/api.go
@ -22,66 +22,66 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r := hs.RouteRegister
|
||||
|
||||
// not logged in views
|
||||
r.Get("/", reqSignedIn, Index)
|
||||
r.Get("/", reqSignedIn, hs.Index)
|
||||
r.Get("/logout", Logout)
|
||||
r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
|
||||
r.Get("/login/:name", quota("session"), OAuthLogin)
|
||||
r.Get("/login", LoginView)
|
||||
r.Get("/invite/:code", Index)
|
||||
r.Get("/login", hs.LoginView)
|
||||
r.Get("/invite/:code", hs.Index)
|
||||
|
||||
// authed views
|
||||
r.Get("/profile/", reqSignedIn, Index)
|
||||
r.Get("/profile/password", reqSignedIn, Index)
|
||||
r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome)
|
||||
r.Get("/org/", reqSignedIn, Index)
|
||||
r.Get("/org/new", reqSignedIn, Index)
|
||||
r.Get("/datasources/", reqSignedIn, Index)
|
||||
r.Get("/datasources/new", reqSignedIn, Index)
|
||||
r.Get("/datasources/edit/*", reqSignedIn, Index)
|
||||
r.Get("/org/users", reqSignedIn, Index)
|
||||
r.Get("/org/users/new", reqSignedIn, Index)
|
||||
r.Get("/org/users/invite", reqSignedIn, Index)
|
||||
r.Get("/org/teams", reqSignedIn, Index)
|
||||
r.Get("/org/teams/*", reqSignedIn, Index)
|
||||
r.Get("/org/apikeys/", reqSignedIn, Index)
|
||||
r.Get("/dashboard/import/", reqSignedIn, Index)
|
||||
r.Get("/configuration", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/settings", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/users", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/users/create", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index)
|
||||
r.Get("/admin/stats", reqGrafanaAdmin, Index)
|
||||
r.Get("/profile/", reqSignedIn, hs.Index)
|
||||
r.Get("/profile/password", reqSignedIn, hs.Index)
|
||||
r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome)
|
||||
r.Get("/org/", reqSignedIn, hs.Index)
|
||||
r.Get("/org/new", reqSignedIn, hs.Index)
|
||||
r.Get("/datasources/", reqSignedIn, hs.Index)
|
||||
r.Get("/datasources/new", reqSignedIn, hs.Index)
|
||||
r.Get("/datasources/edit/*", reqSignedIn, hs.Index)
|
||||
r.Get("/org/users", reqSignedIn, hs.Index)
|
||||
r.Get("/org/users/new", reqSignedIn, hs.Index)
|
||||
r.Get("/org/users/invite", reqSignedIn, hs.Index)
|
||||
r.Get("/org/teams", reqSignedIn, hs.Index)
|
||||
r.Get("/org/teams/*", reqSignedIn, hs.Index)
|
||||
r.Get("/org/apikeys/", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboard/import/", reqSignedIn, hs.Index)
|
||||
r.Get("/configuration", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/settings", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/users", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
|
||||
r.Get("/admin/stats", reqGrafanaAdmin, hs.Index)
|
||||
|
||||
r.Get("/styleguide", reqSignedIn, Index)
|
||||
r.Get("/styleguide", reqSignedIn, hs.Index)
|
||||
|
||||
r.Get("/plugins", reqSignedIn, Index)
|
||||
r.Get("/plugins/:id/edit", reqSignedIn, Index)
|
||||
r.Get("/plugins/:id/page/:page", reqSignedIn, Index)
|
||||
r.Get("/plugins", reqSignedIn, hs.Index)
|
||||
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index)
|
||||
r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index)
|
||||
|
||||
r.Get("/d/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/d/:uid", reqSignedIn, Index)
|
||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index)
|
||||
r.Get("/dashboard/script/*", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", Index)
|
||||
r.Get("/d-solo/:uid/:slug", reqSignedIn, Index)
|
||||
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index)
|
||||
r.Get("/dashboard-solo/script/*", reqSignedIn, Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, Index)
|
||||
r.Get("/dashboards/", reqSignedIn, Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, Index)
|
||||
r.Get("/d/:uid/:slug", reqSignedIn, hs.Index)
|
||||
r.Get("/d/:uid", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, hs.Index)
|
||||
r.Get("/dashboard/script/*", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboard-solo/snapshot/*", hs.Index)
|
||||
r.Get("/d-solo/:uid/:slug", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, hs.Index)
|
||||
r.Get("/dashboard-solo/script/*", reqSignedIn, hs.Index)
|
||||
r.Get("/import/dashboard", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboards/", reqSignedIn, hs.Index)
|
||||
r.Get("/dashboards/*", reqSignedIn, hs.Index)
|
||||
|
||||
r.Get("/explore", reqEditorRole, Index)
|
||||
r.Get("/explore", reqEditorRole, hs.Index)
|
||||
|
||||
r.Get("/playlists/", reqSignedIn, Index)
|
||||
r.Get("/playlists/*", reqSignedIn, Index)
|
||||
r.Get("/alerting/", reqSignedIn, Index)
|
||||
r.Get("/alerting/*", reqSignedIn, Index)
|
||||
r.Get("/playlists/", reqSignedIn, hs.Index)
|
||||
r.Get("/playlists/*", reqSignedIn, hs.Index)
|
||||
r.Get("/alerting/", reqSignedIn, hs.Index)
|
||||
r.Get("/alerting/*", reqSignedIn, hs.Index)
|
||||
|
||||
// sign up
|
||||
r.Get("/signup", Index)
|
||||
r.Get("/signup", hs.Index)
|
||||
r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
|
||||
r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
|
||||
r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
|
||||
@ -91,15 +91,15 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
|
||||
|
||||
// reset password
|
||||
r.Get("/user/password/send-reset-email", Index)
|
||||
r.Get("/user/password/reset", Index)
|
||||
r.Get("/user/password/send-reset-email", hs.Index)
|
||||
r.Get("/user/password/reset", hs.Index)
|
||||
|
||||
r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail))
|
||||
r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword))
|
||||
|
||||
// dashboard snapshots
|
||||
r.Get("/dashboard/snapshot/*", Index)
|
||||
r.Get("/dashboard/snapshots/", reqSignedIn, Index)
|
||||
r.Get("/dashboard/snapshot/*", hs.Index)
|
||||
r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index)
|
||||
|
||||
// api for dashboard snapshots
|
||||
r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot)
|
||||
@ -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)
|
||||
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
@ -251,8 +252,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
return Error(403, err.Error(), err)
|
||||
}
|
||||
|
||||
if err == m.ErrDashboardContainsInvalidAlertData {
|
||||
return Error(500, "Invalid alert data. Cannot save dashboard", err)
|
||||
if validationErr, ok := err.(alerting.ValidationError); ok {
|
||||
return Error(422, validationErr.Error(), nil)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
|
||||
@ -725,7 +726,7 @@ func TestDashboardApiEndpoint(t *testing.T) {
|
||||
{SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412},
|
||||
{SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400},
|
||||
{SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400},
|
||||
{SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500},
|
||||
{SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422},
|
||||
{SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500},
|
||||
{SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500},
|
||||
{SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400},
|
||||
|
@ -55,7 +55,6 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||
|
||||
dsId := c.ParamsInt64(":id")
|
||||
ds, err := hs.getDatasourceFromCache(dsId, c)
|
||||
hs.log.Debug("We are in the ds proxy", "dsId", dsId)
|
||||
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||
|
@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response {
|
||||
return Error(500, "Failed to query datasources", err)
|
||||
}
|
||||
|
||||
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
|
||||
User: c.SignedInUser,
|
||||
Datasources: query.Result,
|
||||
}
|
||||
|
||||
datasources := []*m.DataSource{}
|
||||
if err := bus.Dispatch(&dsFilterQuery); err != nil {
|
||||
if err != bus.ErrHandlerNotFound {
|
||||
return Error(500, "Could not get datasources", err)
|
||||
}
|
||||
|
||||
datasources = query.Result
|
||||
} else {
|
||||
datasources = dsFilterQuery.Result
|
||||
}
|
||||
|
||||
result := make(dtos.DataSourceList, 0)
|
||||
for _, ds := range datasources {
|
||||
for _, ds := range query.Result {
|
||||
dsItem := dtos.DataSourceListItemDTO{
|
||||
OrgId: ds.OrgId,
|
||||
Id: ds.Id,
|
||||
|
@ -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 {
|
||||
@ -133,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,
|
||||
@ -196,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
|
||||
|
@ -28,6 +28,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
@ -52,6 +53,7 @@ type HTTPServer struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
RenderService rendering.Service `inject:""`
|
||||
Cfg *setting.Cfg `inject:""`
|
||||
HooksService *hooks.HooksService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
@ -184,7 +186,7 @@ func (hs *HTTPServer) applyRoutes() {
|
||||
// then custom app proxy routes
|
||||
hs.initAppPluginRoutes(hs.macaron)
|
||||
// lastly not found route
|
||||
hs.macaron.NotFound(NotFoundHandler)
|
||||
hs.macaron.NotFound(hs.NotFoundHandler)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
|
||||
|
@ -17,8 +17,8 @@ const (
|
||||
darkName = "dark"
|
||||
)
|
||||
|
||||
func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
settings, err := getFrontendSettingsMap(c)
|
||||
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
settings, err := hs.getFrontendSettingsMap(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -316,19 +316,6 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
}
|
||||
|
||||
if c.IsGrafanaAdmin {
|
||||
children := []*dtos.NavLink{
|
||||
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
||||
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
||||
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
||||
}
|
||||
|
||||
if setting.IsEnterprise {
|
||||
children = append(children, &dtos.NavLink{Text: "Licensing", Id: "licensing", Url: setting.AppSubUrl + "/admin/licensing", Icon: "fa fa-fw fa-unlock-alt"})
|
||||
}
|
||||
|
||||
children = append(children, &dtos.NavLink{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"})
|
||||
|
||||
cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
|
||||
Text: "Server Admin",
|
||||
HideFromTabs: true,
|
||||
@ -336,7 +323,13 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
Id: "admin",
|
||||
Icon: "gicon gicon-shield",
|
||||
Url: setting.AppSubUrl + "/admin/users",
|
||||
Children: children,
|
||||
Children: []*dtos.NavLink{
|
||||
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
||||
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
||||
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
||||
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
||||
{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -357,11 +350,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||
},
|
||||
})
|
||||
|
||||
hs.HooksService.RunIndexDataHooks(&data)
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
func Index(c *m.ReqContext) {
|
||||
data, err := setIndexViewData(c)
|
||||
func (hs *HTTPServer) Index(c *m.ReqContext) {
|
||||
data, err := hs.setIndexViewData(c)
|
||||
if err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
@ -369,13 +363,13 @@ func Index(c *m.ReqContext) {
|
||||
c.HTML(200, "index", data)
|
||||
}
|
||||
|
||||
func NotFoundHandler(c *m.ReqContext) {
|
||||
func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
|
||||
if c.IsApiRequest() {
|
||||
c.JsonApiErr(404, "Not found", nil)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := setIndexViewData(c)
|
||||
data, err := hs.setIndexViewData(c)
|
||||
if err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
|
@ -17,8 +17,8 @@ const (
|
||||
ViewIndex = "index"
|
||||
)
|
||||
|
||||
func LoginView(c *m.ReqContext) {
|
||||
viewData, err := setIndexViewData(c)
|
||||
func (hs *HTTPServer) LoginView(c *m.ReqContext) {
|
||||
viewData, err := hs.setIndexViewData(c)
|
||||
if err != nil {
|
||||
c.Handle(500, "Failed to get settings", err)
|
||||
return
|
||||
|
@ -177,17 +177,17 @@ func UserSetUsingOrg(c *m.ReqContext) Response {
|
||||
}
|
||||
|
||||
// GET /profile/switch-org/:id
|
||||
func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
|
||||
func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) {
|
||||
orgID := c.ParamsInt64(":id")
|
||||
|
||||
if !validateUsingOrg(c.UserId, orgID) {
|
||||
NotFoundHandler(c)
|
||||
hs.NotFoundHandler(c)
|
||||
}
|
||||
|
||||
cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID}
|
||||
|
||||
if err := bus.Dispatch(&cmd); err != nil {
|
||||
NotFoundHandler(c)
|
||||
hs.NotFoundHandler(c)
|
||||
}
|
||||
|
||||
c.Redirect(setting.AppSubUrl + "/")
|
||||
|
@ -100,7 +100,7 @@ func listenToSystemSignals(server *GrafanaServerImpl) {
|
||||
sighupChan := make(chan os.Signal, 1)
|
||||
|
||||
signal.Notify(sighupChan, syscall.SIGHUP)
|
||||
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
|
||||
signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
@ -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 == false {
|
||||
extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -21,7 +21,6 @@ var (
|
||||
ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else")
|
||||
ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty")
|
||||
ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder")
|
||||
ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard")
|
||||
ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data")
|
||||
ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists")
|
||||
ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id")
|
||||
|
@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct {
|
||||
type DsPermissionType int
|
||||
|
||||
const (
|
||||
DsPermissionQuery DsPermissionType = 1 << iota
|
||||
DsPermissionNoAccess
|
||||
DsPermissionNoAccess DsPermissionType = iota
|
||||
DsPermissionQuery
|
||||
)
|
||||
|
||||
func (p DsPermissionType) String() string {
|
||||
@ -207,12 +207,6 @@ func (p DsPermissionType) String() string {
|
||||
return names[int(p)]
|
||||
}
|
||||
|
||||
type HasRequiredDataSourcePermissionQuery struct {
|
||||
Id int64
|
||||
User *SignedInUser
|
||||
RequiredPermission DsPermissionType
|
||||
}
|
||||
|
||||
type GetDataSourcePermissionsForUserQuery struct {
|
||||
User *SignedInUser
|
||||
Result map[int64]DsPermissionType
|
||||
|
@ -2,6 +2,7 @@ package conditions
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -31,12 +32,12 @@ type ThresholdEvaluator struct {
|
||||
func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) {
|
||||
params := model.Get("params").MustArray()
|
||||
if len(params) == 0 {
|
||||
return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"}
|
||||
return nil, fmt.Errorf("Evaluator missing threshold parameter")
|
||||
}
|
||||
|
||||
firstParam, ok := params[0].(json.Number)
|
||||
if !ok {
|
||||
return nil, alerting.ValidationError{Reason: "Evaluator has invalid parameter"}
|
||||
return nil, fmt.Errorf("Evaluator has invalid parameter")
|
||||
}
|
||||
|
||||
defaultEval := &ThresholdEvaluator{Type: typ}
|
||||
@ -107,7 +108,7 @@ func (e *RangedEvaluator) Eval(reducedValue null.Float) bool {
|
||||
func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
|
||||
typ := model.Get("type").MustString()
|
||||
if typ == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Evaluator missing type property"}
|
||||
return nil, fmt.Errorf("Evaluator missing type property")
|
||||
}
|
||||
|
||||
if inSlice(typ, defaultTypes) {
|
||||
@ -122,7 +123,7 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
|
||||
return &NoValueEvaluator{}, nil
|
||||
}
|
||||
|
||||
return nil, alerting.ValidationError{Reason: "Evaluator invalid evaluator type: " + typ}
|
||||
return nil, fmt.Errorf("Evaluator invalid evaluator type: %s", typ)
|
||||
}
|
||||
|
||||
func inSlice(a string, list []string) bool {
|
||||
|
@ -82,8 +82,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
if collapsed && collapsedJSON.MustBool() {
|
||||
|
||||
// extract alerts from sub panels for collapsed panels
|
||||
alertSlice, err := e.getAlertFromPanels(panel,
|
||||
validateAlertFunc)
|
||||
alertSlice, err := e.getAlertFromPanels(panel, validateAlertFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -100,7 +99,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
|
||||
panelID, err := panel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("panel id is required. err %v", err)
|
||||
return nil, ValidationError{Reason: "A numeric panel id property is missing"}
|
||||
}
|
||||
|
||||
// backward compatibility check, can be removed later
|
||||
@ -146,7 +145,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
|
||||
datasource, err := e.lookupDatasourceID(dsName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
e.log.Debug("Error looking up datasource", "error", err)
|
||||
return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)}
|
||||
}
|
||||
|
||||
jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id)
|
||||
@ -167,8 +167,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json,
|
||||
}
|
||||
|
||||
if !validateAlertFunc(alert) {
|
||||
e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId)
|
||||
return nil, m.ErrDashboardContainsInvalidAlertData
|
||||
return nil, ValidationError{Reason: fmt.Sprintf("Panel id is not correct, alertName=%v, panelId=%v", alert.Name, alert.PanelId)}
|
||||
}
|
||||
|
||||
alerts = append(alerts, alert)
|
||||
|
@ -258,7 +258,7 @@ func TestAlertRuleExtraction(t *testing.T) {
|
||||
|
||||
Convey("Should fail on save", func() {
|
||||
_, err := extractor.GetAlerts()
|
||||
So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData)
|
||||
So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -127,7 +127,13 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval
|
||||
var err error
|
||||
|
||||
imageFile, err = os.Open(evalContext.ImageOnDiskPath)
|
||||
defer imageFile.Close()
|
||||
defer func() {
|
||||
err := imageFile.Close()
|
||||
if err != nil {
|
||||
log.Error2("Could not close Telegram inline image.", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -36,13 +36,13 @@ type ValidationError struct {
|
||||
}
|
||||
|
||||
func (e ValidationError) Error() string {
|
||||
extraInfo := ""
|
||||
extraInfo := e.Reason
|
||||
if e.Alertid != 0 {
|
||||
extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
|
||||
}
|
||||
|
||||
if e.PanelId != 0 {
|
||||
extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId)
|
||||
extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelId)
|
||||
}
|
||||
|
||||
if e.DashboardId != 0 {
|
||||
@ -50,10 +50,10 @@ func (e ValidationError) Error() string {
|
||||
}
|
||||
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo)
|
||||
return fmt.Sprintf("Alert validation error: %s%s", e.Err.Error(), extraInfo)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo)
|
||||
return fmt.Sprintf("Alert validation error: %s", extraInfo)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -128,7 +128,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
}
|
||||
|
||||
if len(model.Conditions) == 0 {
|
||||
return nil, fmt.Errorf("Alert is missing conditions")
|
||||
return nil, ValidationError{Reason: "Alert is missing conditions"}
|
||||
}
|
||||
|
||||
return model, nil
|
||||
|
@ -73,7 +73,7 @@ func (srv *CleanUpService) cleanUpTmpFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files))
|
||||
srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "kept", len(files))
|
||||
}
|
||||
|
||||
func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
@ -25,7 +26,9 @@ type DashboardProvisioningService interface {
|
||||
|
||||
// NewService factory for creating a new dashboard service
|
||||
var NewService = func() DashboardService {
|
||||
return &dashboardServiceImpl{}
|
||||
return &dashboardServiceImpl{
|
||||
log: log.New("dashboard-service"),
|
||||
}
|
||||
}
|
||||
|
||||
// NewProvisioningService factory for creating a new dashboard provisioning service
|
||||
@ -45,6 +48,7 @@ type SaveDashboardDTO struct {
|
||||
type dashboardServiceImpl struct {
|
||||
orgId int64
|
||||
user *models.SignedInUser
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) {
|
||||
@ -89,7 +93,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO,
|
||||
}
|
||||
|
||||
if err := bus.Dispatch(&validateAlertsCmd); err != nil {
|
||||
return nil, models.ErrDashboardContainsInvalidAlertData
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,12 +117,12 @@ func TestDashboardService(t *testing.T) {
|
||||
})
|
||||
|
||||
bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error {
|
||||
return errors.New("error")
|
||||
return errors.New("Alert validation error")
|
||||
})
|
||||
|
||||
dto.Dashboard = models.NewDashboard("Dash")
|
||||
_, err := service.SaveDashboard(dto)
|
||||
So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData)
|
||||
So(err.Error(), ShouldEqual, "Alert validation error")
|
||||
})
|
||||
})
|
||||
|
||||
|
30
pkg/services/hooks/hooks.go
Normal file
30
pkg/services/hooks/hooks.go
Normal file
@ -0,0 +1,30 @@
|
||||
package hooks
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
)
|
||||
|
||||
type IndexDataHook func(indexData *dtos.IndexViewData)
|
||||
|
||||
type HooksService struct {
|
||||
indexDataHooks []IndexDataHook
|
||||
}
|
||||
|
||||
func init() {
|
||||
registry.RegisterService(&HooksService{})
|
||||
}
|
||||
|
||||
func (srv *HooksService) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv *HooksService) AddIndexDataHook(hook IndexDataHook) {
|
||||
srv.indexDataHooks = append(srv.indexDataHooks, hook)
|
||||
}
|
||||
|
||||
func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData) {
|
||||
for _, hook := range srv.indexDataHooks {
|
||||
hook(indexData)
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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]))
|
||||
|
||||
|
@ -16,7 +16,6 @@ func TestUserAuth(t *testing.T) {
|
||||
Convey("Given 5 users", t, func() {
|
||||
var err error
|
||||
var cmd *m.CreateUserCommand
|
||||
users := []m.User{}
|
||||
for i := 0; i < 5; i++ {
|
||||
cmd = &m.CreateUserCommand{
|
||||
Email: fmt.Sprint("user", i, "@test.com"),
|
||||
@ -25,7 +24,6 @@ func TestUserAuth(t *testing.T) {
|
||||
}
|
||||
err = CreateUser(context.Background(), cmd)
|
||||
So(err, ShouldBeNil)
|
||||
users = append(users, cmd.Result)
|
||||
}
|
||||
|
||||
Reset(func() {
|
||||
|
@ -213,6 +213,8 @@ type Cfg struct {
|
||||
TempDataLifetime time.Duration
|
||||
|
||||
MetricsEndpointEnabled bool
|
||||
|
||||
EnableAlphaPanels bool
|
||||
}
|
||||
|
||||
type CommandLineArgs struct {
|
||||
@ -694,6 +696,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) {
|
||||
|
@ -234,10 +234,37 @@ func parseMultiSelectValue(input string) []string {
|
||||
// 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",
|
||||
"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("us-east-1")
|
||||
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})
|
||||
|
@ -52,13 +52,18 @@ func generateConnectionString(datasource *models.DataSource) string {
|
||||
}
|
||||
|
||||
server, port := hostParts[0], hostParts[1]
|
||||
return fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
||||
encrypt := datasource.JsonData.Get("encrypt").MustString("false")
|
||||
connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;",
|
||||
server,
|
||||
port,
|
||||
datasource.Database,
|
||||
datasource.User,
|
||||
password,
|
||||
)
|
||||
if encrypt != "false" {
|
||||
connStr += fmt.Sprintf("encrypt=%s;", encrypt)
|
||||
}
|
||||
return connStr
|
||||
}
|
||||
|
||||
type mssqlRowTransformer struct {
|
||||
|
@ -692,7 +692,7 @@ func TestMSSQL(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
resp, err := endpoint.Query(context.Background(), nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
@ -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,
|
||||
)
|
||||
|
@ -769,7 +769,7 @@ func TestMySQL(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
resp, err := endpoint.Query(context.Background(), nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
@ -701,7 +701,7 @@ func TestPostgres(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := endpoint.Query(nil, nil, query)
|
||||
resp, err := endpoint.Query(context.Background(), nil, query)
|
||||
So(err, ShouldBeNil)
|
||||
queryResult := resp.Results["A"]
|
||||
So(queryResult.Error, ShouldBeNil)
|
||||
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ export class ManageDashboardsCtrl {
|
||||
const template =
|
||||
'<move-to-folder-modal dismiss="dismiss()" ' +
|
||||
'dashboards="model.dashboards" after-save="model.afterSave()">' +
|
||||
'</move-to-folder-modal>`';
|
||||
'</move-to-folder-modal>';
|
||||
appEvents.emit('show-modal', {
|
||||
templateHtml: template,
|
||||
modalClass: 'modal--narrow',
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ export class SideMenu extends PureComponent {
|
||||
render() {
|
||||
return [
|
||||
<div className="sidemenu__logo" onClick={this.toggleSideMenu} key="logo">
|
||||
<img src="public/img/grafana_icon.svg" alt="graphana_logo" />
|
||||
<img src="public/img/grafana_icon.svg" alt="Grafana" />
|
||||
</div>,
|
||||
<div className="sidemenu__logo_small_breakpoint" onClick={this.toggleSideMenuSmallBreakpoint} key="hamburger">
|
||||
<i className="fa fa-bars" />
|
||||
|
@ -8,7 +8,7 @@ Array [
|
||||
onClick={[Function]}
|
||||
>
|
||||
<img
|
||||
alt="graphana_logo"
|
||||
alt="Grafana"
|
||||
src="public/img/grafana_icon.svg"
|
||||
/>
|
||||
</div>,
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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 => {
|
||||
@ -257,7 +257,7 @@ export class KeybindingSrv {
|
||||
});
|
||||
|
||||
this.bind('d r', () => {
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
dashboard.startRefresh();
|
||||
});
|
||||
|
||||
this.bind('d s', () => {
|
||||
|
@ -399,6 +399,77 @@ describe('duration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('clock', () => {
|
||||
it('null', () => {
|
||||
const str = kbn.toClock(null, 0);
|
||||
expect(str).toBe('');
|
||||
});
|
||||
it('size less than 1 second', () => {
|
||||
const str = kbn.toClock(999, 0);
|
||||
expect(str).toBe('999ms');
|
||||
});
|
||||
describe('size less than 1 minute', () => {
|
||||
it('default', () => {
|
||||
const str = kbn.toClock(59999);
|
||||
expect(str).toBe('59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = kbn.toClock(59999, 0);
|
||||
expect(str).toBe('59s');
|
||||
});
|
||||
});
|
||||
describe('size less than 1 hour', () => {
|
||||
it('default', () => {
|
||||
const str = kbn.toClock(3599999);
|
||||
expect(str).toBe('59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = kbn.toClock(3599999, 0);
|
||||
expect(str).toBe('59m');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = kbn.toClock(3599999, 1);
|
||||
expect(str).toBe('59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 1 hour', () => {
|
||||
it('default', () => {
|
||||
const str = kbn.toClock(7199999);
|
||||
expect(str).toBe('01h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = kbn.toClock(7199999, 0);
|
||||
expect(str).toBe('01h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = kbn.toClock(7199999, 1);
|
||||
expect(str).toBe('01h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = kbn.toClock(7199999, 2);
|
||||
expect(str).toBe('01h:59m:59s');
|
||||
});
|
||||
});
|
||||
describe('size greater than or equal 1 day', () => {
|
||||
it('default', () => {
|
||||
const str = kbn.toClock(89999999);
|
||||
expect(str).toBe('24h:59m:59s:999ms');
|
||||
});
|
||||
it('decimals equals 0', () => {
|
||||
const str = kbn.toClock(89999999, 0);
|
||||
expect(str).toBe('24h');
|
||||
});
|
||||
it('decimals equals 1', () => {
|
||||
const str = kbn.toClock(89999999, 1);
|
||||
expect(str).toBe('24h:59m');
|
||||
});
|
||||
it('decimals equals 2', () => {
|
||||
const str = kbn.toClock(89999999, 2);
|
||||
expect(str).toBe('24h:59m:59s');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('volume', () => {
|
||||
it('1000m3', () => {
|
||||
const str = kbn.valueFormats['m3'](1000, 1, null);
|
||||
|
@ -104,5 +104,17 @@ describe('Directed acyclic graph', () => {
|
||||
const actual = nodeH.getOptimizedInputEdges();
|
||||
expect(actual).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('when linking non-existing input node with existing output node should throw error', () => {
|
||||
expect(() => {
|
||||
dag.link('non-existing', 'A');
|
||||
}).toThrowError("cannot link input node named non-existing since it doesn't exist in graph");
|
||||
});
|
||||
|
||||
it('when linking existing input node with non-existing output node should throw error', () => {
|
||||
expect(() => {
|
||||
dag.link('A', 'non-existing');
|
||||
}).toThrowError("cannot link output node named non-existing since it doesn't exist in graph");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -15,6 +15,14 @@ export class Edge {
|
||||
}
|
||||
|
||||
link(inputNode: Node, outputNode: Node) {
|
||||
if (!inputNode) {
|
||||
throw Error('inputNode is required');
|
||||
}
|
||||
|
||||
if (!outputNode) {
|
||||
throw Error('outputNode is required');
|
||||
}
|
||||
|
||||
this.unlink();
|
||||
this.inputNode = inputNode;
|
||||
this.outputNode = outputNode;
|
||||
@ -152,7 +160,11 @@ export class Graph {
|
||||
for (let n = 0; n < inputArr.length; n++) {
|
||||
const i = inputArr[n];
|
||||
if (typeof i === 'string') {
|
||||
inputNodes.push(this.getNode(i));
|
||||
const n = this.getNode(i);
|
||||
if (!n) {
|
||||
throw Error(`cannot link input node named ${i} since it doesn't exist in graph`);
|
||||
}
|
||||
inputNodes.push(n);
|
||||
} else {
|
||||
inputNodes.push(i);
|
||||
}
|
||||
@ -161,7 +173,11 @@ export class Graph {
|
||||
for (let n = 0; n < outputArr.length; n++) {
|
||||
const i = outputArr[n];
|
||||
if (typeof i === 'string') {
|
||||
outputNodes.push(this.getNode(i));
|
||||
const n = this.getNode(i);
|
||||
if (!n) {
|
||||
throw Error(`cannot link output node named ${i} since it doesn't exist in graph`);
|
||||
}
|
||||
outputNodes.push(n);
|
||||
} else {
|
||||
outputNodes.push(i);
|
||||
}
|
||||
|
@ -808,6 +808,51 @@ kbn.toDuration = (size, decimals, timeScale) => {
|
||||
return strings.join(', ');
|
||||
};
|
||||
|
||||
kbn.toClock = (size, decimals) => {
|
||||
if (size === null) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// < 1 second
|
||||
if (size < 1000) {
|
||||
return moment.utc(size).format('SSS\\m\\s');
|
||||
}
|
||||
|
||||
// < 1 minute
|
||||
if (size < 60000) {
|
||||
let format = 'ss\\s:SSS\\m\\s';
|
||||
if (decimals === 0) {
|
||||
format = 'ss\\s';
|
||||
}
|
||||
return moment.utc(size).format(format);
|
||||
}
|
||||
|
||||
// < 1 hour
|
||||
if (size < 3600000) {
|
||||
let format = 'mm\\m:ss\\s:SSS\\m\\s';
|
||||
if (decimals === 0) {
|
||||
format = 'mm\\m';
|
||||
} else if (decimals === 1) {
|
||||
format = 'mm\\m:ss\\s';
|
||||
}
|
||||
return moment.utc(size).format(format);
|
||||
}
|
||||
|
||||
let format = 'mm\\m:ss\\s:SSS\\m\\s';
|
||||
|
||||
const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`;
|
||||
|
||||
if (decimals === 0) {
|
||||
format = '';
|
||||
} else if (decimals === 1) {
|
||||
format = 'mm\\m';
|
||||
} else if (decimals === 2) {
|
||||
format = 'mm\\m:ss\\s';
|
||||
}
|
||||
|
||||
return format ? `${hours}:${moment.utc(size).format(format)}` : hours;
|
||||
};
|
||||
|
||||
kbn.valueFormats.dtdurationms = (size, decimals) => {
|
||||
return kbn.toDuration(size, decimals, 'millisecond');
|
||||
};
|
||||
@ -824,6 +869,14 @@ kbn.valueFormats.timeticks = (size, decimals, scaledDecimals) => {
|
||||
return kbn.valueFormats.s(size / 100, decimals, scaledDecimals);
|
||||
};
|
||||
|
||||
kbn.valueFormats.clockms = (size, decimals) => {
|
||||
return kbn.toClock(size, decimals);
|
||||
};
|
||||
|
||||
kbn.valueFormats.clocks = (size, decimals) => {
|
||||
return kbn.toClock(size * 1000, decimals);
|
||||
};
|
||||
|
||||
kbn.valueFormats.dateTimeAsIso = (epoch, isUtc) => {
|
||||
const time = isUtc ? moment.utc(epoch) : moment(epoch);
|
||||
|
||||
@ -901,6 +954,8 @@ kbn.getUnitFormats = () => {
|
||||
{ text: 'duration (s)', value: 'dtdurations' },
|
||||
{ text: 'duration (hh:mm:ss)', value: 'dthms' },
|
||||
{ text: 'Timeticks (s/100)', value: 'timeticks' },
|
||||
{ text: 'clock (ms)', value: 'clockms' },
|
||||
{ text: 'clock (s)', value: 'clocks' },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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>
|
||||
|
@ -8,6 +8,7 @@ import { makeRegions, dedupAnnotations } from './events_processing';
|
||||
export class AnnotationsSrv {
|
||||
globalAnnotationsPromise: any;
|
||||
alertStatesPromise: any;
|
||||
datasourcePromises: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) {
|
||||
@ -18,6 +19,7 @@ export class AnnotationsSrv {
|
||||
clearCache() {
|
||||
this.globalAnnotationsPromise = null;
|
||||
this.alertStatesPromise = null;
|
||||
this.datasourcePromises = null;
|
||||
}
|
||||
|
||||
getAnnotations(options) {
|
||||
@ -90,6 +92,7 @@ export class AnnotationsSrv {
|
||||
|
||||
const range = this.timeSrv.timeRange();
|
||||
const promises = [];
|
||||
const dsPromises = [];
|
||||
|
||||
for (const annotation of dashboard.annotations.list) {
|
||||
if (!annotation.enable) {
|
||||
@ -99,10 +102,10 @@ export class AnnotationsSrv {
|
||||
if (annotation.snapshotData) {
|
||||
return this.translateQueryResult(annotation, annotation.snapshotData);
|
||||
}
|
||||
|
||||
const datasourcePromise = this.datasourceSrv.get(annotation.datasource);
|
||||
dsPromises.push(datasourcePromise);
|
||||
promises.push(
|
||||
this.datasourceSrv
|
||||
.get(annotation.datasource)
|
||||
datasourcePromise
|
||||
.then(datasource => {
|
||||
// issue query against data source
|
||||
return datasource.annotationQuery({
|
||||
@ -122,7 +125,7 @@ export class AnnotationsSrv {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
this.datasourcePromises = this.$q.all(dsPromises);
|
||||
this.globalAnnotationsPromise = this.$q.all(promises);
|
||||
return this.globalAnnotationsPromise;
|
||||
}
|
||||
|
@ -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,10 @@
|
||||
import config from 'app/core/config';
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { PanelContainer } from './dashgrid/PanelContainer';
|
||||
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 +21,7 @@ export class DashboardCtrl implements PanelContainer {
|
||||
private dashboardSrv,
|
||||
private unsavedChangesSrv,
|
||||
private dashboardViewStateSrv,
|
||||
public playlistSrv,
|
||||
private panelLoader
|
||||
public playlistSrv
|
||||
) {
|
||||
// temp hack due to way dashboards are loaded
|
||||
// can't use controllerAs on route yet
|
||||
@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
|
||||
return this.dashboard;
|
||||
}
|
||||
|
||||
getPanelLoader() {
|
||||
return this.panelLoader;
|
||||
}
|
||||
|
||||
timezoneChanged() {
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
}
|
||||
|
||||
getPanelContainer() {
|
||||
return this;
|
||||
}
|
||||
@ -168,10 +158,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);
|
||||
}
|
||||
}
|
||||
|
@ -200,6 +200,43 @@ export class DashboardModel {
|
||||
this.events.emit('view-mode-changed', panel);
|
||||
}
|
||||
|
||||
timeRangeUpdated() {
|
||||
this.events.emit('time-range-updated');
|
||||
}
|
||||
|
||||
startRefresh() {
|
||||
this.events.emit('refresh');
|
||||
|
||||
for (const panel of this.panels) {
|
||||
if (!this.otherPanelInFullscreen(panel)) {
|
||||
panel.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
this.events.emit('render');
|
||||
|
||||
for (const panel of this.panels) {
|
||||
panel.render();
|
||||
}
|
||||
}
|
||||
|
||||
panelInitialized(panel: PanelModel) {
|
||||
if (!this.otherPanelInFullscreen(panel)) {
|
||||
panel.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
otherPanelInFullscreen(panel: PanelModel) {
|
||||
return this.meta.fullscreen && !panel.fullscreen;
|
||||
}
|
||||
|
||||
changePanelType(panel: PanelModel, pluginId: string) {
|
||||
panel.changeType(pluginId);
|
||||
this.events.emit('panel-type-changed', panel);
|
||||
}
|
||||
|
||||
private ensureListExist(data) {
|
||||
if (!data) {
|
||||
data = {};
|
||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
||||
import classNames from 'classnames';
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { PanelContainer } from './PanelContainer';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
||||
import store from 'app/core/store';
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words';
|
||||
|
||||
export interface AddPanelPanelProps {
|
||||
panel: PanelModel;
|
||||
getPanelContainer: () => PanelContainer;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export interface AddPanelPanelState {
|
||||
@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
}
|
||||
|
||||
onAddPanel = panelPluginInfo => {
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
const dashboard = this.props.dashboard;
|
||||
const { gridPos } = this.props.panel;
|
||||
|
||||
const newPanel: any = {
|
||||
@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
||||
|
||||
handleCloseAddPanel(evt) {
|
||||
evt.preventDefault();
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
dashboard.removePanel(dashboard.panels[0]);
|
||||
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
|
||||
}
|
||||
|
||||
renderText(text: string) {
|
||||
|
@ -3,7 +3,6 @@ import ReactGridLayout from 'react-grid-layout';
|
||||
import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||
import { DashboardPanel } from './DashboardPanel';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelContainer } from './PanelContainer';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import classNames from 'classnames';
|
||||
import sizeMe from 'react-sizeme';
|
||||
@ -60,18 +59,15 @@ function GridWrapper({
|
||||
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
||||
|
||||
export interface DashboardGridProps {
|
||||
getPanelContainer: () => PanelContainer;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
gridToPanelMap: any;
|
||||
panelContainer: PanelContainer;
|
||||
dashboard: DashboardModel;
|
||||
panelMap: { [id: string]: PanelModel };
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.panelContainer = this.props.getPanelContainer();
|
||||
this.onLayoutChange = this.onLayoutChange.bind(this);
|
||||
this.onResize = this.onResize.bind(this);
|
||||
this.onResizeStop = this.onResizeStop.bind(this);
|
||||
@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
this.state = { animated: false };
|
||||
|
||||
// subscribe to dashboard events
|
||||
this.dashboard = this.panelContainer.getDashboard();
|
||||
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
||||
this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
||||
const dashboard = this.props.dashboard;
|
||||
dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
|
||||
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
||||
dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
|
||||
}
|
||||
|
||||
buildLayout() {
|
||||
const layout = [];
|
||||
this.panelMap = {};
|
||||
|
||||
for (const panel of this.dashboard.panels) {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
const stringId = panel.id.toString();
|
||||
this.panelMap[stringId] = panel;
|
||||
|
||||
@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
this.panelMap[newPos.i].updateGridPos(newPos);
|
||||
}
|
||||
|
||||
this.dashboard.sortPanelsByGridPos();
|
||||
this.props.dashboard.sortPanelsByGridPos();
|
||||
}
|
||||
|
||||
triggerForceUpdate() {
|
||||
@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
}
|
||||
|
||||
onWidthChange() {
|
||||
for (const panel of this.dashboard.panels) {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
panel.resizeDone();
|
||||
}
|
||||
}
|
||||
|
||||
onViewModeChanged(payload) {
|
||||
this.setState({ animated: !payload.fullscreen });
|
||||
}
|
||||
|
||||
updateGridPos(item, layout) {
|
||||
this.panelMap[item.i].updateGridPos(item);
|
||||
|
||||
@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
|
||||
componentDidMount() {
|
||||
setTimeout(() => {
|
||||
this.setState(() => {
|
||||
return { animated: true };
|
||||
});
|
||||
this.setState({ animated: true });
|
||||
});
|
||||
}
|
||||
|
||||
renderPanels() {
|
||||
const panelElements = [];
|
||||
|
||||
for (const panel of this.dashboard.panels) {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
|
||||
panelElements.push(
|
||||
/** panel-id is set for html bookmarks */
|
||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id.toString()}`}>
|
||||
<DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
|
||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
|
||||
<DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||
<SizedReactLayoutGrid
|
||||
className={classNames({ layout: true, animated: this.state.animated })}
|
||||
layout={this.buildLayout()}
|
||||
isResizable={this.dashboard.meta.canEdit}
|
||||
isDraggable={this.dashboard.meta.canEdit}
|
||||
isResizable={this.props.dashboard.meta.canEdit}
|
||||
isDraggable={this.props.dashboard.meta.canEdit}
|
||||
onLayoutChange={this.onLayoutChange}
|
||||
onWidthChange={this.onWidthChange}
|
||||
onDragStop={this.onDragStop}
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
import { DashboardGrid } from './DashboardGrid';
|
||||
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [
|
||||
['getPanelContainer', { watchDepth: 'reference', wrapApply: false }],
|
||||
]);
|
||||
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||
|
@ -1,54 +1,161 @@
|
||||
import React from 'react';
|
||||
import {PanelModel} from '../panel_model';
|
||||
import {PanelContainer} from './PanelContainer';
|
||||
import {AttachedPanel} from './PanelLoader';
|
||||
import {DashboardRow} from './DashboardRow';
|
||||
import {AddPanelPanel} from './AddPanelPanel';
|
||||
import config from 'app/core/config';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { DashboardRow } from './DashboardRow';
|
||||
import { AddPanelPanel } from './AddPanelPanel';
|
||||
import { importPluginModule } from 'app/features/plugins/plugin_loader';
|
||||
import { PluginExports, PanelPlugin } from 'app/types/plugins';
|
||||
import { PanelChrome } from './PanelChrome';
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
|
||||
export interface DashboardPanelProps {
|
||||
export interface Props {
|
||||
panelType: string;
|
||||
panel: PanelModel;
|
||||
getPanelContainer: () => PanelContainer;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends React.Component<DashboardPanelProps, any> {
|
||||
export interface State {
|
||||
pluginExports: PluginExports;
|
||||
}
|
||||
|
||||
export class DashboardPanel extends React.Component<Props, State> {
|
||||
element: any;
|
||||
attachedPanel: AttachedPanel;
|
||||
angularPanel: AngularComponent;
|
||||
pluginInfo: any;
|
||||
specialPanels = {};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
|
||||
this.state = {
|
||||
pluginExports: null,
|
||||
};
|
||||
|
||||
this.specialPanels['row'] = this.renderRow.bind(this);
|
||||
this.specialPanels['add-panel'] = this.renderAddPanel.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.element) {
|
||||
isSpecial() {
|
||||
return this.specialPanels[this.props.panel.type];
|
||||
}
|
||||
|
||||
renderRow() {
|
||||
return <DashboardRow panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
renderAddPanel() {
|
||||
return <AddPanelPanel panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
onPluginTypeChanged = (plugin: PanelPlugin) => {
|
||||
this.props.panel.changeType(plugin.id);
|
||||
this.loadPlugin();
|
||||
};
|
||||
|
||||
onAngularPluginTypeChanged = () => {
|
||||
this.loadPlugin();
|
||||
};
|
||||
|
||||
loadPlugin() {
|
||||
if (this.isSpecial()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
const loader = panelContainer.getPanelLoader();
|
||||
this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
|
||||
// handle plugin loading & changing of plugin type
|
||||
if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
|
||||
this.pluginInfo = config.panels[this.props.panel.type];
|
||||
|
||||
if (this.pluginInfo.exports) {
|
||||
this.cleanUpAngularPanel();
|
||||
this.setState({ pluginExports: this.pluginInfo.exports });
|
||||
} else {
|
||||
importPluginModule(this.pluginInfo.module).then(pluginExports => {
|
||||
this.cleanUpAngularPanel();
|
||||
// cache plugin exports (saves a promise async cycle next time)
|
||||
this.pluginInfo.exports = pluginExports;
|
||||
// update panel state
|
||||
this.setState({ pluginExports: pluginExports });
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.loadPlugin();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.loadPlugin();
|
||||
|
||||
// handle angular plugin loading
|
||||
if (!this.element || this.angularPanel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const template = '<plugin-component type="panel" class="panel-height-helper"></plugin-component>';
|
||||
const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard };
|
||||
this.angularPanel = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
cleanUpAngularPanel() {
|
||||
if (this.angularPanel) {
|
||||
this.angularPanel.destroy();
|
||||
this.angularPanel = null;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.attachedPanel) {
|
||||
this.attachedPanel.destroy();
|
||||
}
|
||||
this.cleanUpAngularPanel();
|
||||
}
|
||||
|
||||
renderReactPanel() {
|
||||
const { pluginExports } = this.state;
|
||||
const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper';
|
||||
const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper';
|
||||
|
||||
// this might look strange with these classes that change when edit, but
|
||||
// I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel
|
||||
return (
|
||||
<div className={containerClass}>
|
||||
<div className={panelWrapperClass}>
|
||||
<PanelChrome
|
||||
component={pluginExports.PanelComponent}
|
||||
panel={this.props.panel}
|
||||
dashboard={this.props.dashboard}
|
||||
/>
|
||||
</div>
|
||||
{this.props.panel.isEditing && (
|
||||
<div className="panel-editor-container__editor">
|
||||
<PanelEditor
|
||||
panel={this.props.panel}
|
||||
panelType={this.props.panel.type}
|
||||
dashboard={this.props.dashboard}
|
||||
onTypeChanged={this.onPluginTypeChanged}
|
||||
pluginExports={pluginExports}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
// special handling for rows
|
||||
if (this.props.panel.type === 'row') {
|
||||
return <DashboardRow panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
||||
if (this.isSpecial()) {
|
||||
return this.specialPanels[this.props.panel.type]();
|
||||
}
|
||||
|
||||
if (this.props.panel.type === 'add-panel') {
|
||||
return <AddPanelPanel panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
||||
if (!this.state.pluginExports) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={element => this.element = element} className="panel-height-helper" />
|
||||
);
|
||||
if (this.state.pluginExports.PanelComponent) {
|
||||
return this.renderReactPanel();
|
||||
}
|
||||
|
||||
// legacy angular rendering
|
||||
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,19 +1,16 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { PanelContainer } from './PanelContainer';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
export interface DashboardRowProps {
|
||||
panel: PanelModel;
|
||||
getPanelContainer: () => PanelContainer;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
dashboard: any;
|
||||
panelContainer: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
collapsed: this.props.panel.collapsed,
|
||||
};
|
||||
|
||||
this.panelContainer = this.props.getPanelContainer();
|
||||
this.dashboard = this.panelContainer.getDashboard();
|
||||
|
||||
this.toggle = this.toggle.bind(this);
|
||||
this.openSettings = this.openSettings.bind(this);
|
||||
this.delete = this.delete.bind(this);
|
||||
@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.dashboard.toggleRow(this.props.panel);
|
||||
this.props.dashboard.toggleRow(this.props.panel);
|
||||
|
||||
this.setState(prevState => {
|
||||
return { collapsed: !prevState.collapsed };
|
||||
@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
}
|
||||
|
||||
update() {
|
||||
this.dashboard.processRepeats();
|
||||
this.props.dashboard.processRepeats();
|
||||
this.forceUpdate();
|
||||
}
|
||||
|
||||
@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
altActionText: 'Delete row only',
|
||||
icon: 'fa-trash',
|
||||
onConfirm: () => {
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
dashboard.removeRow(this.props.panel, true);
|
||||
this.props.dashboard.removeRow(this.props.panel, true);
|
||||
},
|
||||
onAltAction: () => {
|
||||
const panelContainer = this.props.getPanelContainer();
|
||||
const dashboard = panelContainer.getDashboard();
|
||||
dashboard.removeRow(this.props.panel, false);
|
||||
this.props.dashboard.removeRow(this.props.panel, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -87,7 +77,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
|
||||
const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
||||
const panels = count === 1 ? 'panel' : 'panels';
|
||||
const canEdit = this.dashboard.meta.canEdit === true;
|
||||
const canEdit = this.props.dashboard.meta.canEdit === true;
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
|
151
public/app/features/dashboard/dashgrid/DataPanel.tsx
Normal file
151
public/app/features/dashboard/dashgrid/DataPanel.tsx
Normal file
@ -0,0 +1,151 @@
|
||||
// Library
|
||||
import React, { Component } from 'react';
|
||||
|
||||
// Services
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
|
||||
// Types
|
||||
import { TimeRange, LoadingState, DataQueryOptions, DataQueryResponse, TimeSeries } from 'app/types';
|
||||
|
||||
interface RenderProps {
|
||||
loading: LoadingState;
|
||||
timeSeries: TimeSeries[];
|
||||
}
|
||||
|
||||
export interface Props {
|
||||
datasource: string | null;
|
||||
queries: any[];
|
||||
panelId?: number;
|
||||
dashboardId?: number;
|
||||
isVisible?: boolean;
|
||||
timeRange?: TimeRange;
|
||||
refreshCounter: number;
|
||||
children: (r: RenderProps) => JSX.Element;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
isFirstLoad: boolean;
|
||||
loading: LoadingState;
|
||||
response: DataQueryResponse;
|
||||
}
|
||||
|
||||
export class DataPanel extends Component<Props, State> {
|
||||
static defaultProps = {
|
||||
isVisible: true,
|
||||
panelId: 1,
|
||||
dashboardId: 1,
|
||||
};
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
loading: LoadingState.NotStarted,
|
||||
response: {
|
||||
data: [],
|
||||
},
|
||||
isFirstLoad: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
console.log('DataPanel mount');
|
||||
}
|
||||
|
||||
async componentDidUpdate(prevProps: Props) {
|
||||
if (!this.hasPropsChanged(prevProps)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.issueQueries();
|
||||
}
|
||||
|
||||
hasPropsChanged(prevProps: Props) {
|
||||
return this.props.refreshCounter !== prevProps.refreshCounter || this.props.isVisible !== prevProps.isVisible;
|
||||
}
|
||||
|
||||
issueQueries = async () => {
|
||||
const { isVisible, queries, datasource, panelId, dashboardId, timeRange } = this.props;
|
||||
|
||||
if (!isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!queries.length) {
|
||||
this.setState({ loading: LoadingState.Done });
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({ loading: LoadingState.Loading });
|
||||
|
||||
try {
|
||||
const dataSourceSrv = getDatasourceSrv();
|
||||
const ds = await dataSourceSrv.get(datasource);
|
||||
|
||||
const queryOptions: DataQueryOptions = {
|
||||
timezone: 'browser',
|
||||
panelId: panelId,
|
||||
dashboardId: dashboardId,
|
||||
range: timeRange,
|
||||
rangeRaw: timeRange.raw,
|
||||
interval: '1s',
|
||||
intervalMs: 60000,
|
||||
targets: queries,
|
||||
maxDataPoints: 500,
|
||||
scopedVars: {},
|
||||
cacheTimeout: null,
|
||||
};
|
||||
|
||||
console.log('Issuing DataPanel query', queryOptions);
|
||||
const resp = await ds.query(queryOptions);
|
||||
console.log('Issuing DataPanel query Resp', resp);
|
||||
|
||||
this.setState({
|
||||
loading: LoadingState.Done,
|
||||
response: resp,
|
||||
isFirstLoad: false,
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Loading error', err);
|
||||
this.setState({ loading: LoadingState.Error, isFirstLoad: false });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { response, loading, isFirstLoad } = this.state;
|
||||
console.log('data panel render');
|
||||
const timeSeries = response.data;
|
||||
|
||||
if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) {
|
||||
return (
|
||||
<div className="loading">
|
||||
<p>Loading</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{this.loadingSpinner}
|
||||
{this.props.children({
|
||||
timeSeries,
|
||||
loading,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private get loadingSpinner(): JSX.Element {
|
||||
const { loading } = this.state;
|
||||
|
||||
if (loading === LoadingState.Loading) {
|
||||
return (
|
||||
<div className="panel__loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
84
public/app/features/dashboard/dashgrid/PanelChrome.tsx
Normal file
84
public/app/features/dashboard/dashgrid/PanelChrome.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
// Libraries
|
||||
import React, { ComponentClass, PureComponent } from 'react';
|
||||
|
||||
// Services
|
||||
import { getTimeSrv } from '../time_srv';
|
||||
|
||||
// Components
|
||||
import { PanelHeader } from './PanelHeader';
|
||||
import { DataPanel } from './DataPanel';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { TimeRange, PanelProps } from 'app/types';
|
||||
|
||||
export interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
component: ComponentClass<PanelProps>;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
refreshCounter: number;
|
||||
timeRange?: TimeRange;
|
||||
}
|
||||
|
||||
export class PanelChrome extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
refreshCounter: 0,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.panel.events.on('refresh', this.onRefresh);
|
||||
this.props.dashboard.panelInitialized(this.props.panel);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.panel.events.off('refresh', this.onRefresh);
|
||||
}
|
||||
|
||||
onRefresh = () => {
|
||||
const timeSrv = getTimeSrv();
|
||||
const timeRange = timeSrv.timeRange();
|
||||
|
||||
this.setState({
|
||||
refreshCounter: this.state.refreshCounter + 1,
|
||||
timeRange: timeRange,
|
||||
});
|
||||
};
|
||||
|
||||
get isVisible() {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { datasource, targets } = panel;
|
||||
const { refreshCounter, timeRange } = this.state;
|
||||
const PanelComponent = this.props.component;
|
||||
|
||||
return (
|
||||
<div className="panel-container">
|
||||
<PanelHeader panel={panel} dashboard={dashboard} />
|
||||
<div className="panel-content">
|
||||
<DataPanel
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
refreshCounter={refreshCounter}
|
||||
>
|
||||
{({ loading, timeSeries }) => {
|
||||
return <PanelComponent loading={loading} timeSeries={timeSeries} timeRange={timeRange} />;
|
||||
}}
|
||||
</DataPanel>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelLoader } from './PanelLoader';
|
||||
|
||||
export interface PanelContainer {
|
||||
getPanelLoader(): PanelLoader;
|
||||
getDashboard(): DashboardModel;
|
||||
}
|
121
public/app/features/dashboard/dashgrid/PanelEditor.tsx
Normal file
121
public/app/features/dashboard/dashgrid/PanelEditor.tsx
Normal file
@ -0,0 +1,121 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { QueriesTab } from './QueriesTab';
|
||||
import { PanelPlugin, PluginExports } from 'app/types/plugins';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
interface PanelEditorProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
panelType: string;
|
||||
pluginExports: PluginExports;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
}
|
||||
|
||||
interface PanelEditorTab {
|
||||
id: string;
|
||||
text: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export class PanelEditor extends React.Component<PanelEditorProps, any> {
|
||||
tabs: PanelEditorTab[];
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.tabs = [
|
||||
{ id: 'queries', text: 'Queries', icon: 'fa fa-database' },
|
||||
{ id: 'visualization', text: 'Visualization', icon: 'fa fa-line-chart' },
|
||||
];
|
||||
}
|
||||
|
||||
renderQueriesTab() {
|
||||
return <QueriesTab panel={this.props.panel} dashboard={this.props.dashboard} />;
|
||||
}
|
||||
|
||||
renderPanelOptions() {
|
||||
const { pluginExports } = this.props;
|
||||
|
||||
if (pluginExports.PanelOptions) {
|
||||
const PanelOptions = pluginExports.PanelOptions;
|
||||
return <PanelOptions />;
|
||||
} else {
|
||||
return <p>Visualization has no options</p>;
|
||||
}
|
||||
}
|
||||
|
||||
renderVizTab() {
|
||||
return (
|
||||
<div className="viz-editor">
|
||||
<div className="viz-editor-col1">
|
||||
<VizTypePicker currentType={this.props.panel.type} onTypeChanged={this.props.onTypeChanged} />
|
||||
</div>
|
||||
<div className="viz-editor-col2">
|
||||
<h5 className="page-heading">Options</h5>
|
||||
{this.renderPanelOptions()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
onChangeTab = (tab: PanelEditorTab) => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: { tab: tab.id },
|
||||
partial: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { location } = store.getState();
|
||||
const activeTab = location.query.tab || 'queries';
|
||||
|
||||
return (
|
||||
<div className="tabbed-view tabbed-view--new">
|
||||
<div className="tabbed-view-header">
|
||||
<ul className="gf-tabs">
|
||||
{this.tabs.map(tab => {
|
||||
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
|
||||
})}
|
||||
</ul>
|
||||
|
||||
<button className="tabbed-view-close-btn" ng-click="ctrl.exitFullscreen();">
|
||||
<i className="fa fa-remove" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tabbed-view-body">
|
||||
{activeTab === 'queries' && this.renderQueriesTab()}
|
||||
{activeTab === 'visualization' && this.renderVizTab()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
interface TabItemParams {
|
||||
tab: PanelEditorTab;
|
||||
activeTab: string;
|
||||
onClick: (tab: PanelEditorTab) => void;
|
||||
}
|
||||
|
||||
function TabItem({ tab, activeTab, onClick }: TabItemParams) {
|
||||
const tabClasses = classNames({
|
||||
'gf-tabs-link': true,
|
||||
active: activeTab === tab.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<li className="gf-tabs-item" key={tab.id}>
|
||||
<a className={tabClasses} onClick={() => onClick(tab)}>
|
||||
<i className={tab.icon} /> {tab.text}
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
}
|
83
public/app/features/dashboard/dashgrid/PanelHeader.tsx
Normal file
83
public/app/features/dashboard/dashgrid/PanelHeader.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { store } from 'app/store/configureStore';
|
||||
import { updateLocation } from 'app/core/actions';
|
||||
|
||||
interface PanelHeaderProps {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class PanelHeader extends React.Component<PanelHeaderProps, any> {
|
||||
onEditPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: this.props.panel.id,
|
||||
edit: true,
|
||||
fullscreen: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
onViewPanel = () => {
|
||||
store.dispatch(
|
||||
updateLocation({
|
||||
query: {
|
||||
panelId: this.props.panel.id,
|
||||
edit: false,
|
||||
fullscreen: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const isFullscreen = false;
|
||||
const isLoading = false;
|
||||
const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen });
|
||||
|
||||
return (
|
||||
<div className={panelHeaderClass}>
|
||||
<span className="panel-info-corner">
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</span>
|
||||
|
||||
{isLoading && (
|
||||
<span className="panel-loading">
|
||||
<i className="fa fa-spinner fa-spin" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="panel-title-container">
|
||||
<span className="panel-title">
|
||||
<span className="icon-gf panel-alert-icon" />
|
||||
<span className="panel-title-text">{this.props.panel.title}</span>
|
||||
<span className="panel-menu-container dropdown">
|
||||
<span className="fa fa-caret-down panel-menu-toggle" data-toggle="dropdown" />
|
||||
<ul className="dropdown-menu dropdown-menu--menu panel-menu" role="menu">
|
||||
<li>
|
||||
<a onClick={this.onEditPanel}>
|
||||
<i className="fa fa-fw fa-edit" /> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a onClick={this.onViewPanel}>
|
||||
<i className="fa fa-fw fa-eye" /> View
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</span>
|
||||
<span className="panel-time-info">
|
||||
<i className="fa fa-clock-o" /> 4m
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
53
public/app/features/dashboard/dashgrid/QueriesTab.tsx
Normal file
53
public/app/features/dashboard/dashgrid/QueriesTab.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Services & utils
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
|
||||
interface Props {
|
||||
panel: PanelModel;
|
||||
dashboard: DashboardModel;
|
||||
}
|
||||
|
||||
export class QueriesTab extends PureComponent<Props> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { panel, dashboard } = this.props;
|
||||
|
||||
const loader = getAngularLoader();
|
||||
const template = '<metrics-tab />';
|
||||
const scopeProps = {
|
||||
ctrl: {
|
||||
panel: panel,
|
||||
dashboard: dashboard,
|
||||
refresh: () => panel.refresh(),
|
||||
},
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this.component) {
|
||||
this.component.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={element => (this.element = element)} className="panel-height-helper" />;
|
||||
}
|
||||
}
|
69
public/app/features/dashboard/dashgrid/VizTypePicker.tsx
Normal file
69
public/app/features/dashboard/dashgrid/VizTypePicker.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import classNames from 'classnames';
|
||||
import config from 'app/core/config';
|
||||
import { PanelPlugin } from 'app/types/plugins';
|
||||
import CustomScrollbar from 'app/core/components/CustomScrollbar/CustomScrollbar';
|
||||
import _ from 'lodash';
|
||||
|
||||
interface Props {
|
||||
currentType: string;
|
||||
onTypeChanged: (newType: PanelPlugin) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
pluginList: PanelPlugin[];
|
||||
}
|
||||
|
||||
export class VizTypePicker extends PureComponent<Props, State> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
pluginList: this.getPanelPlugins(''),
|
||||
};
|
||||
}
|
||||
|
||||
getPanelPlugins(filter) {
|
||||
const panels = _.chain(config.panels)
|
||||
.filter({ hideFromList: false })
|
||||
.map(item => item)
|
||||
.value();
|
||||
|
||||
// add sort by sort property
|
||||
return _.sortBy(panels, 'sort');
|
||||
}
|
||||
|
||||
renderVizPlugin = (plugin, index) => {
|
||||
const cssClass = classNames({
|
||||
'viz-picker__item': true,
|
||||
'viz-picker__item--selected': plugin.id === this.props.currentType,
|
||||
});
|
||||
|
||||
return (
|
||||
<div key={index} className={cssClass} onClick={() => this.props.onTypeChanged(plugin)} title={plugin.name}>
|
||||
<img className="viz-picker__item-img" src={plugin.info.logos.small} />
|
||||
<div className="viz-picker__item-name">{plugin.name}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="viz-picker">
|
||||
<div className="viz-picker__search">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input type="text" className="gf-form-input" placeholder="Search type" />
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="viz-picker__items">
|
||||
<CustomScrollbar>
|
||||
<div className="scroll-margin-helper">{this.state.pluginList.map(this.renderVizPlugin)}</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
@ -42,6 +42,8 @@ export class DashNavCtrl {
|
||||
} else if (search.fullscreen) {
|
||||
delete search.fullscreen;
|
||||
delete search.edit;
|
||||
delete search.tab;
|
||||
delete search.panelId;
|
||||
}
|
||||
this.$location.search(search);
|
||||
}
|
||||
|
@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
||||
events: true,
|
||||
fullscreen: true,
|
||||
isEditing: true,
|
||||
hasRefreshed: true,
|
||||
};
|
||||
|
||||
const defaults: any = {
|
||||
gridPos: { x: 0, y: 0, h: 3, w: 6 },
|
||||
datasource: null,
|
||||
targets: [{}],
|
||||
};
|
||||
|
||||
export class PanelModel {
|
||||
@ -31,10 +38,14 @@ export class PanelModel {
|
||||
collapsed?: boolean;
|
||||
panels?: any;
|
||||
soloMode?: boolean;
|
||||
targets: any[];
|
||||
datasource: string;
|
||||
thresholds?: any;
|
||||
|
||||
// non persisted
|
||||
fullscreen: boolean;
|
||||
isEditing: boolean;
|
||||
hasRefreshed: boolean;
|
||||
events: Emitter;
|
||||
|
||||
constructor(model) {
|
||||
@ -45,9 +56,8 @@ export class PanelModel {
|
||||
this[property] = model[property];
|
||||
}
|
||||
|
||||
if (!this.gridPos) {
|
||||
this.gridPos = { x: 0, y: 0, h: 3, w: 6 };
|
||||
}
|
||||
// defaults
|
||||
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||
}
|
||||
|
||||
getSaveModel() {
|
||||
@ -57,6 +67,10 @@ export class PanelModel {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_.isEqual(this[property], defaults[property])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
model[property] = _.cloneDeep(this[property]);
|
||||
}
|
||||
|
||||
@ -82,7 +96,6 @@ export class PanelModel {
|
||||
this.gridPos.h = newPos.h;
|
||||
|
||||
if (sizeChanged) {
|
||||
console.log('PanelModel sizeChanged event and render events fired');
|
||||
this.events.emit('panel-size-changed');
|
||||
}
|
||||
}
|
||||
@ -91,6 +104,34 @@ export class PanelModel {
|
||||
this.events.emit('panel-size-changed');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.hasRefreshed = true;
|
||||
this.events.emit('refresh');
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.hasRefreshed) {
|
||||
this.refresh();
|
||||
} else {
|
||||
this.events.emit('render');
|
||||
}
|
||||
}
|
||||
|
||||
panelInitialized() {
|
||||
this.events.emit('panel-initialized');
|
||||
}
|
||||
|
||||
initEditMode() {
|
||||
this.events.emit('panel-init-edit-mode');
|
||||
}
|
||||
|
||||
changeType(pluginId: string) {
|
||||
this.type = pluginId;
|
||||
|
||||
delete this.thresholds;
|
||||
delete this.alert;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.events.removeAllListeners();
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ export class SettingsCtrl {
|
||||
|
||||
this.$scope.$on('$destroy', () => {
|
||||
this.dashboard.updateSubmenuVisibility();
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
this.dashboard.startRefresh();
|
||||
setTimeout(() => {
|
||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||
});
|
||||
|
@ -46,8 +46,7 @@ export class ShareSnapshotCtrl {
|
||||
|
||||
$scope.loading = true;
|
||||
$scope.snapshot.external = external;
|
||||
|
||||
$rootScope.$broadcast('refresh');
|
||||
$scope.dashboard.startRefresh();
|
||||
|
||||
$timeout(() => {
|
||||
$scope.saveSnapshot(external);
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user