mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'upstream/master' into graph-legend-to-react
This commit is contained in:
commit
5a4c362985
@ -238,8 +238,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
- run:
|
||||||
name: build, test and package grafana enterprise
|
name: prepare build tools
|
||||||
command: './scripts/build/build_enterprise.sh'
|
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:
|
- run:
|
||||||
name: sign packages
|
name: sign packages
|
||||||
command: './scripts/build/sign_packages.sh'
|
command: './scripts/build/sign_packages.sh'
|
||||||
@ -254,6 +263,53 @@ jobs:
|
|||||||
paths:
|
paths:
|
||||||
- enterprise-dist/grafana-enterprise*
|
- 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:
|
deploy-enterprise-master:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:2.7-stretch
|
- image: circleci/python:2.7-stretch
|
||||||
@ -267,6 +323,19 @@ jobs:
|
|||||||
name: deploy to s3
|
name: deploy to s3
|
||||||
command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master'
|
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:
|
deploy-master:
|
||||||
docker:
|
docker:
|
||||||
- image: circleci/python:2.7-stretch
|
- image: circleci/python:2.7-stretch
|
||||||
@ -313,7 +382,7 @@ workflows:
|
|||||||
jobs:
|
jobs:
|
||||||
- build-all:
|
- build-all:
|
||||||
filters: *filter-only-master
|
filters: *filter-only-master
|
||||||
- build-enterprise:
|
- build-all-enterprise:
|
||||||
filters: *filter-only-master
|
filters: *filter-only-master
|
||||||
- codespell:
|
- codespell:
|
||||||
filters: *filter-only-master
|
filters: *filter-only-master
|
||||||
@ -356,13 +425,15 @@ workflows:
|
|||||||
- gometalinter
|
- gometalinter
|
||||||
- mysql-integration-test
|
- mysql-integration-test
|
||||||
- postgres-integration-test
|
- postgres-integration-test
|
||||||
- build-enterprise
|
- build-all-enterprise
|
||||||
filters: *filter-only-master
|
filters: *filter-only-master
|
||||||
|
|
||||||
release:
|
release:
|
||||||
jobs:
|
jobs:
|
||||||
- build-all:
|
- build-all:
|
||||||
filters: *filter-only-release
|
filters: *filter-only-release
|
||||||
|
- build-all-enterprise:
|
||||||
|
filters: *filter-only-release
|
||||||
- codespell:
|
- codespell:
|
||||||
filters: *filter-only-release
|
filters: *filter-only-release
|
||||||
- gometalinter:
|
- gometalinter:
|
||||||
@ -385,6 +456,17 @@ workflows:
|
|||||||
- mysql-integration-test
|
- mysql-integration-test
|
||||||
- postgres-integration-test
|
- postgres-integration-test
|
||||||
filters: *filter-only-release
|
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:
|
- grafana-docker-release:
|
||||||
requires:
|
requires:
|
||||||
- build-all
|
- build-all
|
||||||
|
18
CHANGELOG.md
18
CHANGELOG.md
@ -2,25 +2,37 @@
|
|||||||
|
|
||||||
### New Features
|
### 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)
|
* **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
|
### Minor
|
||||||
|
|
||||||
* **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu)
|
* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda)
|
||||||
* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg)
|
* **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)
|
||||||
|
|
||||||
### Breaking changes
|
### 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)
|
* 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)
|
* **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)
|
* **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)
|
* **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)
|
* **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**: 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)
|
* **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)
|
* **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)
|
* **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
|
* **HTTP API**: Fix /api/org/users so that query and limit querystrings works
|
||||||
|
30
README.md
30
README.md
@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
|
|||||||
|
|
||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Go 1.11
|
- Go (Latest Stable)
|
||||||
- NodeJS LTS
|
- NodeJS LTS
|
||||||
|
|
||||||
### Building the backend
|
### 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`).
|
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`
|
1. Build the frontend `go run build.go build-frontend`
|
||||||
2. Build the docker image `make build-docker-dev`
|
2. Build the docker image `make build-docker-dev`
|
||||||
|
|
||||||
The resulting image will be tagged as `grafana/grafana: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
|
### Dev config
|
||||||
|
|
||||||
Create a custom.ini in the conf directory to override default configuration options.
|
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/...
|
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
|
## Contribute
|
||||||
|
|
||||||
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
|
||||||
|
89
UPGRADING_DEPENDENCIES.md
Normal file
89
UPGRADING_DEPENDENCIES.md
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
# Guide to Upgrading Dependencies
|
||||||
|
|
||||||
|
Upgrading Go or Node.js requires making changes in many different files. See below for a list and explanation for each.
|
||||||
|
|
||||||
|
## Go
|
||||||
|
|
||||||
|
- CircleCi
|
||||||
|
- `grafana/build-container`
|
||||||
|
- Appveyor
|
||||||
|
- Dockerfile
|
||||||
|
|
||||||
|
## Node.js
|
||||||
|
|
||||||
|
- CircleCI
|
||||||
|
- `grafana/build-container`
|
||||||
|
- Appveyor
|
||||||
|
- Dockerfile
|
||||||
|
|
||||||
|
## Go Dependencies
|
||||||
|
|
||||||
|
Updated using `dep`.
|
||||||
|
|
||||||
|
- `Gopkg.toml`
|
||||||
|
- `Gopkg.lock`
|
||||||
|
|
||||||
|
## Node.js Dependencies
|
||||||
|
|
||||||
|
Updated using `yarn`.
|
||||||
|
|
||||||
|
- `package.json`
|
||||||
|
|
||||||
|
## Where to make changes
|
||||||
|
|
||||||
|
### CircleCI
|
||||||
|
|
||||||
|
Our builds run on CircleCI through our build script.
|
||||||
|
|
||||||
|
#### Files
|
||||||
|
|
||||||
|
- `.circleci/config.yml`.
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
|
||||||
|
- nodejs
|
||||||
|
- golang
|
||||||
|
- grafana/build-container (our custom docker build container)
|
||||||
|
|
||||||
|
### grafana/build-container
|
||||||
|
|
||||||
|
The main build step (in CircleCI) is built using a custom build container that comes pre-baked with some of the neccesary dependencies.
|
||||||
|
|
||||||
|
Link: [grafana-build-container](https://github.com/grafana/grafana-build-container)
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
|
||||||
|
- fpm
|
||||||
|
- nodejs
|
||||||
|
- golang
|
||||||
|
- crosscompiling (several compilers)
|
||||||
|
|
||||||
|
### Appveyor
|
||||||
|
|
||||||
|
Master and release builds trigger test runs on Appveyors build environment so that tests will run on Windows.
|
||||||
|
|
||||||
|
#### Files:
|
||||||
|
|
||||||
|
- `appveyor.yml`
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
|
||||||
|
- nodejs
|
||||||
|
- golang
|
||||||
|
|
||||||
|
### Dockerfile
|
||||||
|
|
||||||
|
There is a Docker build for Grafana in the root of the project that allows anyone to build Grafana just using Docker.
|
||||||
|
|
||||||
|
#### Files
|
||||||
|
|
||||||
|
- `Dockerfile`
|
||||||
|
|
||||||
|
#### Dependencies
|
||||||
|
|
||||||
|
- nodejs
|
||||||
|
- golang
|
||||||
|
|
||||||
|
### Local developer environments
|
||||||
|
|
||||||
|
Please send out a notice in the grafana-dev slack channel when updating Go or Node.js to make it easier for everyone to update their local developer environments.
|
@ -5,7 +5,7 @@ os: Windows Server 2012 R2
|
|||||||
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
clone_folder: c:\gopath\src\github.com\grafana\grafana
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
nodejs_version: "6"
|
nodejs_version: "8"
|
||||||
GOPATH: C:\gopath
|
GOPATH: C:\gopath
|
||||||
GOVERSION: 1.11
|
GOVERSION: 1.11
|
||||||
|
|
||||||
|
@ -554,3 +554,6 @@ container_name =
|
|||||||
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
# Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer
|
||||||
server_url =
|
server_url =
|
||||||
callback_url =
|
callback_url =
|
||||||
|
|
||||||
|
[panels]
|
||||||
|
enable_alpha = false
|
||||||
|
@ -166,6 +166,7 @@ Since not all datasources have the same configuration settings we only have the
|
|||||||
| tsdbVersion | string | OpenTSDB | Version |
|
| tsdbVersion | string | OpenTSDB | Version |
|
||||||
| tsdbResolution | string | OpenTSDB | Resolution |
|
| tsdbResolution | string | OpenTSDB | Resolution |
|
||||||
| sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' |
|
| 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 |
|
| 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 |
|
| timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension |
|
||||||
| maxOpenConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of open connections to the database (Grafana v5.4+) |
|
| 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:
|
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".
|
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
|
## IAM Policies
|
||||||
|
|
||||||
Grafana needs permissions granted via IAM to be able to read CloudWatch metrics
|
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.
|
utilize Grafana's built-in support for assuming roles.
|
||||||
|
|
||||||
Here is a minimal policy example:
|
Here is a minimal policy example:
|
||||||
@ -65,11 +65,12 @@ Here is a minimal policy example:
|
|||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Sid": "AllowReadingTagsFromEC2",
|
"Sid": "AllowReadingTagsInstancesRegionsFromEC2",
|
||||||
"Effect": "Allow",
|
"Effect": "Allow",
|
||||||
"Action": [
|
"Action": [
|
||||||
"ec2:DescribeTags",
|
"ec2:DescribeTags",
|
||||||
"ec2:DescribeInstances"
|
"ec2:DescribeInstances",
|
||||||
|
"ec2:DescribeRegions"
|
||||||
],
|
],
|
||||||
"Resource": "*"
|
"Resource": "*"
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ Name | Description
|
|||||||
*Database* | Name of your MSSQL database.
|
*Database* | Name of your MSSQL database.
|
||||||
*User* | Database user's login/username
|
*User* | Database user's login/username
|
||||||
*Password* | Database user's password
|
*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 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 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+).
|
*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
|
### 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.
|
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.
|
||||||
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 possible, we recommend you to use the latest service pack available for optimal compatibility.
|
||||||
|
|
||||||
## Query Editor
|
## Query Editor
|
||||||
|
|
||||||
|
@ -206,6 +206,7 @@ datasources:
|
|||||||
jsonData:
|
jsonData:
|
||||||
tokenUri: https://oauth2.googleapis.com/token
|
tokenUri: https://oauth2.googleapis.com/token
|
||||||
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
clientEmail: stackdriver@myproject.iam.gserviceaccount.com
|
||||||
|
defaultProject: my-project-name
|
||||||
secureJsonData:
|
secureJsonData:
|
||||||
privateKey: |
|
privateKey: |
|
||||||
-----BEGIN PRIVATE KEY-----
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
@ -87,7 +87,7 @@ docker run \
|
|||||||
|
|
||||||
## Building a custom Grafana image with pre-installed plugins
|
## 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:
|
Example of how to build and run:
|
||||||
```bash
|
```bash
|
||||||
@ -103,6 +103,21 @@ docker run \
|
|||||||
grafana:latest-with-plugins
|
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
|
## Configuring AWS Credentials for CloudWatch Support
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
|
|||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
- [Go 1.11](https://golang.org/dl/)
|
- [Go (Latest Stable)](https://golang.org/dl/)
|
||||||
- [Git](https://git-scm.com/downloads)
|
- [Git](https://git-scm.com/downloads)
|
||||||
- [NodeJS LTS](https://nodejs.org/download/)
|
- [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.
|
- 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",
|
"stable": "5.3.1",
|
||||||
"testing": "5.3.0"
|
"testing": "5.3.1"
|
||||||
}
|
}
|
||||||
|
@ -160,6 +160,7 @@
|
|||||||
"react-redux": "^5.0.7",
|
"react-redux": "^5.0.7",
|
||||||
"react-select": "2.1.0",
|
"react-select": "2.1.0",
|
||||||
"react-sizeme": "^2.3.6",
|
"react-sizeme": "^2.3.6",
|
||||||
|
"react-table": "^6.8.6",
|
||||||
"react-transition-group": "^2.2.1",
|
"react-transition-group": "^2.2.1",
|
||||||
"redux": "^4.0.0",
|
"redux": "^4.0.0",
|
||||||
"redux-logger": "^3.0.6",
|
"redux-logger": "^3.0.6",
|
||||||
|
@ -234,13 +234,13 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
datasourceRoute.Get("/", Wrap(GetDataSources))
|
datasourceRoute.Get("/", Wrap(GetDataSources))
|
||||||
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
|
datasourceRoute.Post("/", quota("data_source"), bind(m.AddDataSourceCommand{}), Wrap(AddDataSource))
|
||||||
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
datasourceRoute.Put("/:id", bind(m.UpdateDataSourceCommand{}), Wrap(UpdateDataSource))
|
||||||
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceByID))
|
datasourceRoute.Delete("/:id", Wrap(DeleteDataSourceById))
|
||||||
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
datasourceRoute.Delete("/name/:name", Wrap(DeleteDataSourceByName))
|
||||||
datasourceRoute.Get("/:id", Wrap(GetDataSourceByID))
|
datasourceRoute.Get("/:id", Wrap(GetDataSourceById))
|
||||||
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
|
datasourceRoute.Get("/name/:name", Wrap(GetDataSourceByName))
|
||||||
}, reqOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIDByName), reqSignedIn)
|
apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn)
|
||||||
|
|
||||||
apiRoute.Get("/plugins", Wrap(GetPluginList))
|
apiRoute.Get("/plugins", Wrap(GetPluginList))
|
||||||
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
|
apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID))
|
||||||
@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting))
|
||||||
}, reqOrgAdmin)
|
}, 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)
|
||||||
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest)
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
"github.com/grafana/grafana/pkg/api/pluginproxy"
|
||||||
@ -14,6 +15,20 @@ import (
|
|||||||
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
const HeaderNameNoBackendCache = "X-Grafana-NoCache"
|
||||||
|
|
||||||
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
|
func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) {
|
||||||
|
userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{
|
||||||
|
User: c.SignedInUser,
|
||||||
|
}
|
||||||
|
if err := bus.Dispatch(&userPermissionsQuery); err != nil {
|
||||||
|
if err != bus.ErrHandlerNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
permissionType, exists := userPermissionsQuery.Result[id]
|
||||||
|
if exists && permissionType != m.DsPermissionQuery {
|
||||||
|
return nil, errors.New("User not allowed to access datasource")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true"
|
||||||
cacheKey := fmt.Sprintf("ds-%d", id)
|
cacheKey := fmt.Sprintf("ds-%d", id)
|
||||||
|
|
||||||
@ -38,7 +53,9 @@ func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.Data
|
|||||||
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) {
|
||||||
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer)
|
||||||
|
|
||||||
ds, err := hs.getDatasourceFromCache(c.ParamsInt64(":id"), c)
|
dsId := c.ParamsInt64(":id")
|
||||||
|
ds, err := hs.getDatasourceFromCache(dsId, c)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
c.JsonApiErr(500, "Unable to load datasource meta data", err)
|
||||||
return
|
return
|
||||||
|
@ -20,8 +20,8 @@ func GetDataSources(c *m.ReqContext) Response {
|
|||||||
result := make(dtos.DataSourceList, 0)
|
result := make(dtos.DataSourceList, 0)
|
||||||
for _, ds := range query.Result {
|
for _, ds := range query.Result {
|
||||||
dsItem := dtos.DataSourceListItemDTO{
|
dsItem := dtos.DataSourceListItemDTO{
|
||||||
Id: ds.Id,
|
|
||||||
OrgId: ds.OrgId,
|
OrgId: ds.OrgId,
|
||||||
|
Id: ds.Id,
|
||||||
Name: ds.Name,
|
Name: ds.Name,
|
||||||
Url: ds.Url,
|
Url: ds.Url,
|
||||||
Type: ds.Type,
|
Type: ds.Type,
|
||||||
@ -49,7 +49,7 @@ func GetDataSources(c *m.ReqContext) Response {
|
|||||||
return JSON(200, &result)
|
return JSON(200, &result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetDataSourceByID(c *m.ReqContext) Response {
|
func GetDataSourceById(c *m.ReqContext) Response {
|
||||||
query := m.GetDataSourceByIdQuery{
|
query := m.GetDataSourceByIdQuery{
|
||||||
Id: c.ParamsInt64(":id"),
|
Id: c.ParamsInt64(":id"),
|
||||||
OrgId: c.OrgId,
|
OrgId: c.OrgId,
|
||||||
@ -68,14 +68,14 @@ func GetDataSourceByID(c *m.ReqContext) Response {
|
|||||||
return JSON(200, &dtos)
|
return JSON(200, &dtos)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteDataSourceByID(c *m.ReqContext) Response {
|
func DeleteDataSourceById(c *m.ReqContext) Response {
|
||||||
id := c.ParamsInt64(":id")
|
id := c.ParamsInt64(":id")
|
||||||
|
|
||||||
if id <= 0 {
|
if id <= 0 {
|
||||||
return Error(400, "Missing valid datasource id", nil)
|
return Error(400, "Missing valid datasource id", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
ds, err := getRawDataSourceByID(id, c.OrgId)
|
ds, err := getRawDataSourceById(id, c.OrgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Error(400, "Failed to delete datasource", nil)
|
return Error(400, "Failed to delete datasource", nil)
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ds, err := getRawDataSourceByID(cmd.Id, cmd.OrgId)
|
ds, err := getRawDataSourceById(cmd.Id, cmd.OrgId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -206,7 +206,7 @@ func fillWithSecureJSONData(cmd *m.UpdateDataSourceCommand) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getRawDataSourceByID(id int64, orgID int64) (*m.DataSource, error) {
|
func getRawDataSourceById(id int64, orgID int64) (*m.DataSource, error) {
|
||||||
query := m.GetDataSourceByIdQuery{
|
query := m.GetDataSourceByIdQuery{
|
||||||
Id: id,
|
Id: id,
|
||||||
OrgId: orgID,
|
OrgId: orgID,
|
||||||
@ -236,7 +236,7 @@ func GetDataSourceByName(c *m.ReqContext) Response {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get /api/datasources/id/:name
|
// Get /api/datasources/id/:name
|
||||||
func GetDataSourceIDByName(c *m.ReqContext) Response {
|
func GetDataSourceIdByName(c *m.ReqContext) Response {
|
||||||
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
|
query := m.GetDataSourceByNameQuery{Name: c.Params(":name"), OrgId: c.OrgId}
|
||||||
|
|
||||||
if err := bus.Dispatch(&query); err != nil {
|
if err := bus.Dispatch(&query); err != nil {
|
||||||
|
@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string {
|
|||||||
|
|
||||||
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
||||||
return &AlertNotification{
|
return &AlertNotification{
|
||||||
Id: notification.Id,
|
Id: notification.Id,
|
||||||
Name: notification.Name,
|
Name: notification.Name,
|
||||||
Type: notification.Type,
|
Type: notification.Type,
|
||||||
IsDefault: notification.IsDefault,
|
IsDefault: notification.IsDefault,
|
||||||
Created: notification.Created,
|
Created: notification.Created,
|
||||||
Updated: notification.Updated,
|
Updated: notification.Updated,
|
||||||
Frequency: formatShort(notification.Frequency),
|
Frequency: formatShort(notification.Frequency),
|
||||||
SendReminder: notification.SendReminder,
|
SendReminder: notification.SendReminder,
|
||||||
Settings: notification.Settings,
|
DisableResolveMessage: notification.DisableResolveMessage,
|
||||||
|
Settings: notification.Settings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertNotification struct {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
IsDefault bool `json:"isDefault"`
|
IsDefault bool `json:"isDefault"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
Frequency string `json:"frequency"`
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||||
Created time.Time `json:"created"`
|
Frequency string `json:"frequency"`
|
||||||
Updated time.Time `json:"updated"`
|
Created time.Time `json:"created"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
Updated time.Time `json:"updated"`
|
||||||
|
Settings *simplejson.Json `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertTestCommand struct {
|
type AlertTestCommand struct {
|
||||||
@ -100,11 +102,12 @@ type EvalMatch struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NotificationTestCommand struct {
|
type NotificationTestCommand struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
Frequency string `json:"frequency"`
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
Frequency string `json:"frequency"`
|
||||||
|
Settings *simplejson.Json `json:"settings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PauseAlertCommand struct {
|
type PauseAlertCommand struct {
|
||||||
|
@ -11,7 +11,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/util"
|
"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)
|
orgDataSources := make([]*m.DataSource, 0)
|
||||||
|
|
||||||
if c.OrgId != 0 {
|
if c.OrgId != 0 {
|
||||||
@ -22,7 +22,20 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
orgDataSources = query.Result
|
dsFilterQuery := m.DatasourcesPermissionFilterQuery{
|
||||||
|
User: c.SignedInUser,
|
||||||
|
Datasources: query.Result,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(&dsFilterQuery); err != nil {
|
||||||
|
if err != bus.ErrHandlerNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
orgDataSources = query.Result
|
||||||
|
} else {
|
||||||
|
orgDataSources = dsFilterQuery.Result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
datasources := make(map[string]interface{})
|
datasources := make(map[string]interface{})
|
||||||
@ -120,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
|
|||||||
|
|
||||||
panels := map[string]interface{}{}
|
panels := map[string]interface{}{}
|
||||||
for _, panel := range enabledPlugins.Panels {
|
for _, panel := range enabledPlugins.Panels {
|
||||||
|
if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
panels[panel.Id] = map[string]interface{}{
|
panels[panel.Id] = map[string]interface{}{
|
||||||
"module": panel.Module,
|
"module": panel.Module,
|
||||||
"baseUrl": panel.BaseUrl,
|
"baseUrl": panel.BaseUrl,
|
||||||
@ -183,8 +200,8 @@ func getPanelSort(id string) int {
|
|||||||
return sort
|
return sort
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetFrontendSettings(c *m.ReqContext) {
|
func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := hs.getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JsonApiErr(400, "Failed to get frontend settings", err)
|
c.JsonApiErr(400, "Failed to get frontend settings", err)
|
||||||
return
|
return
|
||||||
|
@ -18,7 +18,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
||||||
settings, err := getFrontendSettingsMap(c)
|
settings, err := hs.getFrontendSettingsMap(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
|
|||||||
|
|
||||||
if ldapUser.isMemberOf(group.GroupDN) {
|
if ldapUser.isMemberOf(group.GroupDN) {
|
||||||
extUser.OrgRoles[group.OrgId] = group.OrgRole
|
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 {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
Frequency time.Duration `json:"frequency"`
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||||
IsDefault bool `json:"isDefault"`
|
Frequency time.Duration `json:"frequency"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
IsDefault bool `json:"isDefault"`
|
||||||
Created time.Time `json:"created"`
|
Settings *simplejson.Json `json:"settings"`
|
||||||
Updated time.Time `json:"updated"`
|
Created time.Time `json:"created"`
|
||||||
|
Updated time.Time `json:"updated"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CreateAlertNotificationCommand struct {
|
type CreateAlertNotificationCommand struct {
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
Type string `json:"type" binding:"Required"`
|
Type string `json:"type" binding:"Required"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
Frequency string `json:"frequency"`
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||||
IsDefault bool `json:"isDefault"`
|
Frequency string `json:"frequency"`
|
||||||
Settings *simplejson.Json `json:"settings"`
|
IsDefault bool `json:"isDefault"`
|
||||||
|
Settings *simplejson.Json `json:"settings"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateAlertNotificationCommand struct {
|
type UpdateAlertNotificationCommand struct {
|
||||||
Id int64 `json:"id" binding:"Required"`
|
Id int64 `json:"id" binding:"Required"`
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
Type string `json:"type" binding:"Required"`
|
Type string `json:"type" binding:"Required"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
Frequency string `json:"frequency"`
|
DisableResolveMessage bool `json:"disableResolveMessage"`
|
||||||
IsDefault bool `json:"isDefault"`
|
Frequency string `json:"frequency"`
|
||||||
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
IsDefault bool `json:"isDefault"`
|
||||||
|
Settings *simplejson.Json `json:"settings" binding:"Required"`
|
||||||
|
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
|
@ -30,6 +30,7 @@ var (
|
|||||||
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
ErrDataSourceNameExists = errors.New("Data source with same name already exists")
|
||||||
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
ErrDataSourceUpdatingOldVersion = errors.New("Trying to update old version of datasource")
|
||||||
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
|
ErrDatasourceIsReadOnly = errors.New("Data source is readonly. Can only be updated from configuration.")
|
||||||
|
ErrDataSourceAccessDenied = errors.New("Data source access denied")
|
||||||
)
|
)
|
||||||
|
|
||||||
type DsAccess string
|
type DsAccess string
|
||||||
@ -167,6 +168,7 @@ type DeleteDataSourceByNameCommand struct {
|
|||||||
|
|
||||||
type GetDataSourcesQuery struct {
|
type GetDataSourcesQuery struct {
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
User *SignedInUser
|
||||||
Result []*DataSource
|
Result []*DataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +189,31 @@ type GetDataSourceByNameQuery struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------
|
// ---------------------
|
||||||
// EVENTS
|
// Permissions
|
||||||
type DataSourceCreatedEvent struct {
|
// ---------------------
|
||||||
|
|
||||||
|
type DsPermissionType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
DsPermissionNoAccess DsPermissionType = iota
|
||||||
|
DsPermissionQuery
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p DsPermissionType) String() string {
|
||||||
|
names := map[int]string{
|
||||||
|
int(DsPermissionQuery): "Query",
|
||||||
|
int(DsPermissionNoAccess): "No Access",
|
||||||
|
}
|
||||||
|
return names[int(p)]
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetDataSourcePermissionsForUserQuery struct {
|
||||||
|
User *SignedInUser
|
||||||
|
Result map[int64]DsPermissionType
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatasourcesPermissionFilterQuery struct {
|
||||||
|
User *SignedInUser
|
||||||
|
Datasources []*DataSource
|
||||||
|
Result []*DataSource
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,7 @@ type Notifier interface {
|
|||||||
GetNotifierId() int64
|
GetNotifierId() int64
|
||||||
GetIsDefault() bool
|
GetIsDefault() bool
|
||||||
GetSendReminder() bool
|
GetSendReminder() bool
|
||||||
|
GetDisableResolveMessage() bool
|
||||||
GetFrequency() time.Duration
|
GetFrequency() time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/log"
|
"github.com/grafana/grafana/pkg/log"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -15,13 +14,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type NotifierBase struct {
|
type NotifierBase struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Id int64
|
Id int64
|
||||||
IsDeault bool
|
IsDeault bool
|
||||||
UploadImage bool
|
UploadImage bool
|
||||||
SendReminder bool
|
SendReminder bool
|
||||||
Frequency time.Duration
|
DisableResolveMessage bool
|
||||||
|
Frequency time.Duration
|
||||||
|
|
||||||
log log.Logger
|
log log.Logger
|
||||||
}
|
}
|
||||||
@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NotifierBase{
|
return NotifierBase{
|
||||||
Id: model.Id,
|
Id: model.Id,
|
||||||
Name: model.Name,
|
Name: model.Name,
|
||||||
IsDeault: model.IsDefault,
|
IsDeault: model.IsDefault,
|
||||||
Type: model.Type,
|
Type: model.Type,
|
||||||
UploadImage: uploadImage,
|
UploadImage: uploadImage,
|
||||||
SendReminder: model.SendReminder,
|
SendReminder: model.SendReminder,
|
||||||
Frequency: model.Frequency,
|
DisableResolveMessage: model.DisableResolveMessage,
|
||||||
log: log.New("alerting.notifier." + model.Name),
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool {
|
|||||||
return n.SendReminder
|
return n.SendReminder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NotifierBase) GetDisableResolveMessage() bool {
|
||||||
|
return n.DisableResolveMessage
|
||||||
|
}
|
||||||
|
|
||||||
func (n *NotifierBase) GetFrequency() time.Duration {
|
func (n *NotifierBase) GetFrequency() time.Duration {
|
||||||
return n.Frequency
|
return n.Frequency
|
||||||
}
|
}
|
||||||
|
@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) {
|
|||||||
base := NewNotifierBase(model)
|
base := NewNotifierBase(model)
|
||||||
So(base.UploadImage, ShouldBeTrue)
|
So(base.UploadImage, ShouldBeTrue)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("default value should be false for backwards compatibility", func() {
|
||||||
|
base := NewNotifierBase(model)
|
||||||
|
So(base.DisableResolveMessage, ShouldBeFalse)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
|
|||||||
alert_notification.updated,
|
alert_notification.updated,
|
||||||
alert_notification.settings,
|
alert_notification.settings,
|
||||||
alert_notification.is_default,
|
alert_notification.is_default,
|
||||||
|
alert_notification.disable_resolve_message,
|
||||||
alert_notification.send_reminder,
|
alert_notification.send_reminder,
|
||||||
alert_notification.frequency
|
alert_notification.frequency
|
||||||
FROM alert_notification
|
FROM alert_notification
|
||||||
@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
|
|||||||
alert_notification.updated,
|
alert_notification.updated,
|
||||||
alert_notification.settings,
|
alert_notification.settings,
|
||||||
alert_notification.is_default,
|
alert_notification.is_default,
|
||||||
|
alert_notification.disable_resolve_message,
|
||||||
alert_notification.send_reminder,
|
alert_notification.send_reminder,
|
||||||
alert_notification.frequency
|
alert_notification.frequency
|
||||||
FROM alert_notification
|
FROM alert_notification
|
||||||
@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
alertNotification := &m.AlertNotification{
|
alertNotification := &m.AlertNotification{
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
Type: cmd.Type,
|
Type: cmd.Type,
|
||||||
Settings: cmd.Settings,
|
Settings: cmd.Settings,
|
||||||
SendReminder: cmd.SendReminder,
|
SendReminder: cmd.SendReminder,
|
||||||
Frequency: frequency,
|
DisableResolveMessage: cmd.DisableResolveMessage,
|
||||||
Created: time.Now(),
|
Frequency: frequency,
|
||||||
Updated: time.Now(),
|
Created: time.Now(),
|
||||||
IsDefault: cmd.IsDefault,
|
Updated: time.Now(),
|
||||||
|
IsDefault: cmd.IsDefault,
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil {
|
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.Type = cmd.Type
|
||||||
current.IsDefault = cmd.IsDefault
|
current.IsDefault = cmd.IsDefault
|
||||||
current.SendReminder = cmd.SendReminder
|
current.SendReminder = cmd.SendReminder
|
||||||
|
current.DisableResolveMessage = cmd.DisableResolveMessage
|
||||||
|
|
||||||
if current.SendReminder {
|
if current.SendReminder {
|
||||||
if cmd.Frequency == "" {
|
if cmd.Frequency == "" {
|
||||||
@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
|||||||
current.Frequency = frequency
|
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 {
|
if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
So(cmd.Result.OrgId, ShouldNotEqual, 0)
|
So(cmd.Result.OrgId, ShouldNotEqual, 0)
|
||||||
So(cmd.Result.Type, ShouldEqual, "email")
|
So(cmd.Result.Type, ShouldEqual, "email")
|
||||||
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
||||||
|
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
|
||||||
|
|
||||||
Convey("Cannot save Alert Notification with the same name", func() {
|
Convey("Cannot save Alert Notification with the same name", func() {
|
||||||
err = CreateAlertNotificationCommand(cmd)
|
err = CreateAlertNotificationCommand(cmd)
|
||||||
@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
|
|
||||||
Convey("Can update alert notification", func() {
|
Convey("Can update alert notification", func() {
|
||||||
newCmd := &models.UpdateAlertNotificationCommand{
|
newCmd := &models.UpdateAlertNotificationCommand{
|
||||||
Name: "NewName",
|
Name: "NewName",
|
||||||
Type: "webhook",
|
Type: "webhook",
|
||||||
OrgId: cmd.Result.OrgId,
|
OrgId: cmd.Result.OrgId,
|
||||||
SendReminder: true,
|
SendReminder: true,
|
||||||
Frequency: "60s",
|
DisableResolveMessage: true,
|
||||||
Settings: simplejson.New(),
|
Frequency: "60s",
|
||||||
Id: cmd.Result.Id,
|
Settings: simplejson.New(),
|
||||||
|
Id: cmd.Result.Id,
|
||||||
}
|
}
|
||||||
err := UpdateAlertNotification(newCmd)
|
err := UpdateAlertNotification(newCmd)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(newCmd.Result.Name, ShouldEqual, "NewName")
|
So(newCmd.Result.Name, ShouldEqual, "NewName")
|
||||||
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
|
So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second)
|
||||||
|
So(newCmd.Result.DisableResolveMessage, ShouldBeTrue)
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Can update alert notification to disable sending of reminders", func() {
|
Convey("Can update alert notification to disable sending of reminders", func() {
|
||||||
|
@ -27,6 +27,7 @@ func GetDataSourceById(query *m.GetDataSourceByIdQuery) error {
|
|||||||
|
|
||||||
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
|
datasource := m.DataSource{OrgId: query.OrgId, Id: query.Id}
|
||||||
has, err := x.Get(&datasource)
|
has, err := x.Get(&datasource)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) {
|
|||||||
mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
|
mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{
|
||||||
Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0",
|
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]))
|
mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0]))
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ type SqlStore struct {
|
|||||||
dbCfg DatabaseConfig
|
dbCfg DatabaseConfig
|
||||||
engine *xorm.Engine
|
engine *xorm.Engine
|
||||||
log log.Logger
|
log log.Logger
|
||||||
|
Dialect migrator.Dialect
|
||||||
skipEnsureAdmin bool
|
skipEnsureAdmin bool
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +126,12 @@ func (ss *SqlStore) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ss.engine = engine
|
ss.engine = engine
|
||||||
|
ss.Dialect = migrator.NewDialect(ss.engine)
|
||||||
|
|
||||||
// temporarily still set global var
|
// temporarily still set global var
|
||||||
x = engine
|
x = engine
|
||||||
dialect = migrator.NewDialect(x)
|
dialect = ss.Dialect
|
||||||
|
|
||||||
migrator := migrator.NewMigrator(x)
|
migrator := migrator.NewMigrator(x)
|
||||||
migrations.AddMigrations(migrator)
|
migrations.AddMigrations(migrator)
|
||||||
|
|
||||||
@ -347,7 +350,11 @@ func InitTestDB(t *testing.T) *SqlStore {
|
|||||||
t.Fatalf("Failed to init test database: %v", err)
|
t.Fatalf("Failed to init test database: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dialect = migrator.NewDialect(engine)
|
sqlstore.Dialect = migrator.NewDialect(engine)
|
||||||
|
|
||||||
|
// temp global var until we get rid of global vars
|
||||||
|
dialect = sqlstore.Dialect
|
||||||
|
|
||||||
if err := dialect.CleanDB(); err != nil {
|
if err := dialect.CleanDB(); err != nil {
|
||||||
t.Fatalf("Failed to clean test db %v", err)
|
t.Fatalf("Failed to clean test db %v", err)
|
||||||
}
|
}
|
||||||
|
@ -213,6 +213,8 @@ type Cfg struct {
|
|||||||
TempDataLifetime time.Duration
|
TempDataLifetime time.Duration
|
||||||
|
|
||||||
MetricsEndpointEnabled bool
|
MetricsEndpointEnabled bool
|
||||||
|
|
||||||
|
EnableAlphaPanels bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type CommandLineArgs struct {
|
type CommandLineArgs struct {
|
||||||
@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error {
|
|||||||
explore := iniFile.Section("explore")
|
explore := iniFile.Section("explore")
|
||||||
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
ExploreEnabled = explore.Key("enabled").MustBool(false)
|
||||||
|
|
||||||
|
panels := iniFile.Section("panels")
|
||||||
|
cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false)
|
||||||
|
|
||||||
cfg.readSessionConfig()
|
cfg.readSessionConfig()
|
||||||
cfg.readSmtpSettings()
|
cfg.readSmtpSettings()
|
||||||
cfg.readQuotaSettings()
|
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) {
|
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),
|
Results: make(map[string]*tsdb.QueryResult),
|
||||||
}
|
}
|
||||||
|
resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries))
|
||||||
|
|
||||||
eg, ectx := errgroup.WithContext(ctx)
|
eg, ectx := errgroup.WithContext(ctx)
|
||||||
|
|
||||||
@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
|||||||
RefId := queryContext.Queries[i].RefId
|
RefId := queryContext.Queries[i].RefId
|
||||||
query, err := parseQuery(queryContext.Queries[i].Model)
|
query, err := parseQuery(queryContext.Queries[i].Model)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Results[RefId] = &tsdb.QueryResult{
|
results.Results[RefId] = &tsdb.QueryResult{
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
return result, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
query.RefId = RefId
|
query.RefId = RefId
|
||||||
|
|
||||||
@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if query.Id == "" && query.Expression != "" {
|
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"),
|
Error: fmt.Errorf("Invalid query: id should be set if using expression"),
|
||||||
}
|
}
|
||||||
return result, nil
|
return results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
eg.Go(func() error {
|
eg.Go(func() error {
|
||||||
@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Results[query.RefId] = &tsdb.QueryResult{
|
resultChan <- &tsdb.QueryResult{
|
||||||
|
RefId: query.RefId,
|
||||||
Error: err,
|
Error: err,
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
result.Results[queryRes.RefId] = queryRes
|
resultChan <- queryRes
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, queryRes := range queryResponses {
|
for _, queryRes := range queryResponses {
|
||||||
result.Results[queryRes.RefId] = queryRes
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Results[queryRes.RefId].Error = err
|
queryRes.Error = err
|
||||||
}
|
}
|
||||||
|
resultChan <- queryRes
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo
|
|||||||
if err := eg.Wait(); err != nil {
|
if err := eg.Wait(); err != nil {
|
||||||
return nil, err
|
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) {
|
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
|
// 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) {
|
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||||
regions := []string{
|
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",
|
"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-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",
|
"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)
|
result := make([]suggestData, 0)
|
||||||
for _, region := range regions {
|
for _, region := range regions {
|
||||||
result = append(result, suggestData{Text: region, Value: region})
|
result = append(result, suggestData{Text: region, Value: region})
|
||||||
|
@ -52,13 +52,18 @@ func generateConnectionString(datasource *models.DataSource) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
server, port := hostParts[0], hostParts[1]
|
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,
|
server,
|
||||||
port,
|
port,
|
||||||
datasource.Database,
|
datasource.Database,
|
||||||
datasource.User,
|
datasource.User,
|
||||||
password,
|
password,
|
||||||
)
|
)
|
||||||
|
if encrypt != "false" {
|
||||||
|
connStr += fmt.Sprintf("encrypt=%s;", encrypt)
|
||||||
|
}
|
||||||
|
return connStr
|
||||||
}
|
}
|
||||||
|
|
||||||
type mssqlRowTransformer struct {
|
type mssqlRowTransformer struct {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/go-sql-driver/mysql"
|
"github.com/go-sql-driver/mysql"
|
||||||
"github.com/go-xorm/core"
|
"github.com/go-xorm/core"
|
||||||
@ -20,10 +21,14 @@ func init() {
|
|||||||
func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||||
logger := log.New("tsdb.mysql")
|
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",
|
cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true",
|
||||||
datasource.User,
|
datasource.User,
|
||||||
datasource.Password,
|
datasource.Password,
|
||||||
"tcp",
|
protocol,
|
||||||
datasource.Url,
|
datasource.Url,
|
||||||
datasource.Database,
|
datasource.Database,
|
||||||
)
|
)
|
||||||
|
@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => {
|
|||||||
return array;
|
return array;
|
||||||
};
|
};
|
||||||
|
|
||||||
import { coreModule, registerAngularDirectives } from './core/core';
|
import { coreModule, angularModules } from 'app/core/core_module';
|
||||||
import { setupAngularRoutes } from './routes/routes';
|
import { registerAngularDirectives } from 'app/core/core';
|
||||||
|
import { setupAngularRoutes } from 'app/routes/routes';
|
||||||
|
|
||||||
|
import 'app/routes/GrafanaCtrl';
|
||||||
|
import 'app/features/all';
|
||||||
|
|
||||||
// import symlinked extensions
|
// import symlinked extensions
|
||||||
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/);
|
||||||
@ -109,39 +113,26 @@ export class GrafanaApp {
|
|||||||
'react',
|
'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
|
// makes it possible to add dynamic stuff
|
||||||
this.useModule(coreModule);
|
_.each(angularModules, m => {
|
||||||
|
this.useModule(m);
|
||||||
|
});
|
||||||
|
|
||||||
// register react angular wrappers
|
// register react angular wrappers
|
||||||
coreModule.config(setupAngularRoutes);
|
coreModule.config(setupAngularRoutes);
|
||||||
registerAngularDirectives();
|
registerAngularDirectives();
|
||||||
|
|
||||||
const preBootRequires = [import('app/features/all')];
|
// disable tool tip animation
|
||||||
|
$.fn.tooltip.defaults.animation = false;
|
||||||
|
|
||||||
Promise.all(preBootRequires)
|
// bootstrap the app
|
||||||
.then(() => {
|
angular.bootstrap(document, this.ngModuleDependencies).invoke(() => {
|
||||||
// disable tool tip animation
|
_.each(this.preBootModules, module => {
|
||||||
$.fn.tooltip.defaults.animation = false;
|
_.extend(module, this.registerFunctions);
|
||||||
|
|
||||||
// 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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.preBootModules = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class AddPermissions extends Component<Props, NewDashboardAclItem> {
|
class AddPermissions extends Component<Props, NewDashboardAclItem> {
|
||||||
|
static defaultProps = {
|
||||||
|
showPermissionLevels: true,
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = this.getCleanState();
|
this.state = this.getCleanState();
|
||||||
|
@ -22,10 +22,6 @@ export interface Props {
|
|||||||
const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
|
const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
|
||||||
|
|
||||||
class DescriptionPicker extends Component<Props, any> {
|
class DescriptionPicker extends Component<Props, any> {
|
||||||
constructor(props) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
|
const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
|
||||||
const selectedOption = getSelectedOption(optionsWithDesc, value);
|
const selectedOption = getSelectedOption(optionsWithDesc, value);
|
||||||
|
@ -103,7 +103,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
|||||||
$scope.$apply(() => {
|
$scope.$apply(() => {
|
||||||
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
|
$scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => {
|
||||||
const dynamicOptions = _.map(result, op => {
|
const dynamicOptions = _.map(result, op => {
|
||||||
return op.value;
|
return _.escape(op.value);
|
||||||
});
|
});
|
||||||
callback(dynamicOptions);
|
callback(dynamicOptions);
|
||||||
});
|
});
|
||||||
@ -117,6 +117,7 @@ export function queryPartEditorDirective($compile, templateSrv) {
|
|||||||
minLength: 0,
|
minLength: 0,
|
||||||
items: 1000,
|
items: 1000,
|
||||||
updater: value => {
|
updater: value => {
|
||||||
|
value = _.unescape(value);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
inputBlur.call($input[0], paramIndex);
|
inputBlur.call($input[0], paramIndex);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
@ -18,6 +18,7 @@ export function geminiScrollbar() {
|
|||||||
let scrollRoot = elem.parent();
|
let scrollRoot = elem.parent();
|
||||||
const scroller = elem;
|
const scroller = elem;
|
||||||
|
|
||||||
|
console.log('scroll');
|
||||||
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') {
|
||||||
scrollRoot = scroller;
|
scrollRoot = scroller;
|
||||||
}
|
}
|
||||||
|
@ -109,12 +109,12 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
|||||||
$scope.$apply(() => {
|
$scope.$apply(() => {
|
||||||
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
|
$scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => {
|
||||||
const dynamicOptions = _.map(result, op => {
|
const dynamicOptions = _.map(result, op => {
|
||||||
return op.value;
|
return _.escape(op.value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// add current value to dropdown if it's not in dynamicOptions
|
// add current value to dropdown if it's not in dynamicOptions
|
||||||
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
|
if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) {
|
||||||
dynamicOptions.unshift(part.params[paramIndex]);
|
dynamicOptions.unshift(_.escape(part.params[paramIndex]));
|
||||||
}
|
}
|
||||||
|
|
||||||
callback(dynamicOptions);
|
callback(dynamicOptions);
|
||||||
@ -129,6 +129,7 @@ export function sqlPartEditorDirective($compile, templateSrv) {
|
|||||||
minLength: 0,
|
minLength: 0,
|
||||||
items: 1000,
|
items: 1000,
|
||||||
updater: value => {
|
updater: value => {
|
||||||
|
value = _.unescape(value);
|
||||||
if (value === part.params[paramIndex]) {
|
if (value === part.params[paramIndex]) {
|
||||||
clearTimeout(cancelBlur);
|
clearTimeout(cancelBlur);
|
||||||
$input.focus();
|
$input.focus();
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
import { PanelPlugin } from 'app/types/plugins';
|
||||||
|
|
||||||
export interface BuildInfo {
|
export interface BuildInfo {
|
||||||
version: string;
|
version: string;
|
||||||
@ -9,7 +10,7 @@ export interface BuildInfo {
|
|||||||
|
|
||||||
export class Settings {
|
export class Settings {
|
||||||
datasources: any;
|
datasources: any;
|
||||||
panels: any;
|
panels: PanelPlugin[];
|
||||||
appSubUrl: string;
|
appSubUrl: string;
|
||||||
windowTitlePrefix: string;
|
windowTitlePrefix: string;
|
||||||
buildInfo: BuildInfo;
|
buildInfo: BuildInfo;
|
||||||
|
@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250;
|
|||||||
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3;
|
||||||
|
|
||||||
export const LS_PANEL_COPY_KEY = 'panel-copy';
|
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/search_srv';
|
||||||
import './services/ng_react';
|
import './services/ng_react';
|
||||||
|
|
||||||
import { grafanaAppDirective } from './components/grafana_app';
|
|
||||||
import { searchDirective } from './components/search/search';
|
import { searchDirective } from './components/search/search';
|
||||||
import { infoPopover } from './components/info_popover';
|
import { infoPopover } from './components/info_popover';
|
||||||
import { navbarDirective } from './components/navbar/navbar';
|
import { navbarDirective } from './components/navbar/navbar';
|
||||||
@ -60,7 +59,6 @@ export {
|
|||||||
registerAngularDirectives,
|
registerAngularDirectives,
|
||||||
arrayJoin,
|
arrayJoin,
|
||||||
coreModule,
|
coreModule,
|
||||||
grafanaAppDirective,
|
|
||||||
navbarDirective,
|
navbarDirective,
|
||||||
searchDirective,
|
searchDirective,
|
||||||
liveSrv,
|
liveSrv,
|
||||||
|
@ -1,2 +1,18 @@
|
|||||||
import angular from 'angular';
|
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';
|
import coreModule from '../core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function dashClass() {
|
function dashClass($timeout) {
|
||||||
return {
|
return {
|
||||||
link: ($scope, elem) => {
|
link: ($scope, elem) => {
|
||||||
$scope.onAppEvent('panel-fullscreen-enter', () => {
|
$scope.ctrl.dashboard.events.on('view-mode-changed', panel => {
|
||||||
elem.toggleClass('panel-in-fullscreen', true);
|
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', $scope.ctrl.dashboard.meta.fullscreen === true);
|
||||||
elem.toggleClass('panel-in-fullscreen', false);
|
|
||||||
});
|
|
||||||
|
|
||||||
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
$scope.$watch('ctrl.dashboardViewState.state.editview', newValue => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
|
@ -3,7 +3,7 @@ import $ from 'jquery';
|
|||||||
import coreModule from '../core_module';
|
import coreModule from '../core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function metricSegment($compile, $sce) {
|
export function metricSegment($compile, $sce, templateSrv) {
|
||||||
const inputTemplate =
|
const inputTemplate =
|
||||||
'<input type="text" data-provide="typeahead" ' +
|
'<input type="text" data-provide="typeahead" ' +
|
||||||
' class="gf-form-input input-medium"' +
|
' class="gf-form-input input-medium"' +
|
||||||
@ -41,13 +41,11 @@ export function metricSegment($compile, $sce) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = _.unescape(value);
|
|
||||||
|
|
||||||
$scope.$apply(() => {
|
$scope.$apply(() => {
|
||||||
const selected = _.find($scope.altSegments, { value: value });
|
const selected = _.find($scope.altSegments, { value: value });
|
||||||
if (selected) {
|
if (selected) {
|
||||||
segment.value = selected.value;
|
segment.value = selected.value;
|
||||||
segment.html = selected.html || selected.value;
|
segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value));
|
||||||
segment.fake = false;
|
segment.fake = false;
|
||||||
segment.expandable = selected.expandable;
|
segment.expandable = selected.expandable;
|
||||||
|
|
||||||
@ -56,7 +54,7 @@ export function metricSegment($compile, $sce) {
|
|||||||
}
|
}
|
||||||
} else if (segment.custom !== 'false') {
|
} else if (segment.custom !== 'false') {
|
||||||
segment.value = value;
|
segment.value = value;
|
||||||
segment.html = $sce.trustAsHtml(value);
|
segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value));
|
||||||
segment.expandable = true;
|
segment.expandable = true;
|
||||||
segment.fake = false;
|
segment.fake = false;
|
||||||
}
|
}
|
||||||
@ -95,7 +93,7 @@ export function metricSegment($compile, $sce) {
|
|||||||
// add custom values
|
// add custom values
|
||||||
if (segment.custom !== 'false') {
|
if (segment.custom !== 'false') {
|
||||||
if (!segment.fake && _.indexOf(options, segment.value) === -1) {
|
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 => {
|
$scope.updater = value => {
|
||||||
|
value = _.unescape(value);
|
||||||
if (value === segment.value) {
|
if (value === segment.value) {
|
||||||
clearTimeout(cancelBlur);
|
clearTimeout(cancelBlur);
|
||||||
$input.focus();
|
$input.focus();
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Action } from 'app/core/actions/location';
|
import { Action } from 'app/core/actions/location';
|
||||||
import { LocationState } from 'app/types';
|
import { LocationState } from 'app/types';
|
||||||
import { renderUrl } from 'app/core/utils/url';
|
import { renderUrl } from 'app/core/utils/url';
|
||||||
|
import _ from 'lodash';
|
||||||
|
|
||||||
export const initialState: LocationState = {
|
export const initialState: LocationState = {
|
||||||
url: '',
|
url: '',
|
||||||
@ -12,11 +13,17 @@ export const initialState: LocationState = {
|
|||||||
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
export const locationReducer = (state = initialState, action: Action): LocationState => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'UPDATE_LOCATION': {
|
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 {
|
return {
|
||||||
url: renderUrl(path || state.path, query),
|
url: renderUrl(path || state.path, query),
|
||||||
path: path || state.path,
|
path: path || state.path,
|
||||||
query: query || state.query,
|
query: query,
|
||||||
routeParams: routeParams || state.routeParams,
|
routeParams: routeParams || state.routeParams,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ import coreModule from '../core_module';
|
|||||||
|
|
||||||
class DynamicDirectiveSrv {
|
class DynamicDirectiveSrv {
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $compile, private $rootScope) {}
|
constructor(private $compile) {}
|
||||||
|
|
||||||
addDirective(element, name, scope) {
|
addDirective(element, name, scope) {
|
||||||
const child = angular.element(document.createElement(name));
|
const child = angular.element(document.createElement(name));
|
||||||
@ -14,25 +14,19 @@ class DynamicDirectiveSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
link(scope, elem, attrs, options) {
|
link(scope, elem, attrs, options) {
|
||||||
options
|
const directiveInfo = options.directive(scope);
|
||||||
.directive(scope)
|
if (!directiveInfo || !directiveInfo.fn) {
|
||||||
.then(directiveInfo => {
|
elem.empty();
|
||||||
if (!directiveInfo || !directiveInfo.fn) {
|
return;
|
||||||
elem.empty();
|
}
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!directiveInfo.fn.registered) {
|
if (!directiveInfo.fn.registered) {
|
||||||
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
console.log('register panel tab');
|
||||||
directiveInfo.fn.registered = true;
|
coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn);
|
||||||
}
|
directiveInfo.fn.registered = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.addDirective(elem, directiveInfo.name, scope);
|
this.addDirective(elem, directiveInfo.name, scope);
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log('Plugin load:', err);
|
|
||||||
this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
create(options) {
|
create(options) {
|
||||||
|
@ -148,7 +148,7 @@ export class KeybindingSrv {
|
|||||||
this.bind('mod+o', () => {
|
this.bind('mod+o', () => {
|
||||||
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
|
||||||
appEvents.emit('graph-hover-clear');
|
appEvents.emit('graph-hover-clear');
|
||||||
this.$rootScope.$broadcast('refresh');
|
dashboard.startRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('mod+s', e => {
|
this.bind('mod+s', e => {
|
||||||
@ -257,7 +257,7 @@ export class KeybindingSrv {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d r', () => {
|
this.bind('d r', () => {
|
||||||
this.$rootScope.$broadcast('refresh');
|
dashboard.startRefresh();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.bind('d s', () => {
|
this.bind('d s', () => {
|
||||||
|
@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl {
|
|||||||
defaults: any = {
|
defaults: any = {
|
||||||
type: 'email',
|
type: 'email',
|
||||||
sendReminder: false,
|
sendReminder: false,
|
||||||
|
disableResolveMessage: false,
|
||||||
frequency: '15m',
|
frequency: '15m',
|
||||||
settings: {
|
settings: {
|
||||||
httpMethod: 'POST',
|
httpMethod: 'POST',
|
||||||
|
@ -21,21 +21,28 @@
|
|||||||
<gf-form-switch
|
<gf-form-switch
|
||||||
class="gf-form"
|
class="gf-form"
|
||||||
label="Send on all alerts"
|
label="Send on all alerts"
|
||||||
label-class="width-12"
|
label-class="width-14"
|
||||||
checked="ctrl.model.isDefault"
|
checked="ctrl.model.isDefault"
|
||||||
tooltip="Use this notification for all alerts">
|
tooltip="Use this notification for all alerts">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
<gf-form-switch
|
<gf-form-switch
|
||||||
class="gf-form"
|
class="gf-form"
|
||||||
label="Include image"
|
label="Include image"
|
||||||
label-class="width-12"
|
label-class="width-14"
|
||||||
checked="ctrl.model.settings.uploadImage"
|
checked="ctrl.model.settings.uploadImage"
|
||||||
tooltip="Captures an image and include it in the notification">
|
tooltip="Captures an image and include it in the notification">
|
||||||
</gf-form-switch>
|
</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
|
<gf-form-switch
|
||||||
class="gf-form"
|
class="gf-form"
|
||||||
label="Send reminders"
|
label="Send reminders"
|
||||||
label-class="width-12"
|
label-class="width-14"
|
||||||
checked="ctrl.model.sendReminder"
|
checked="ctrl.model.sendReminder"
|
||||||
tooltip="Send additional notifications for triggered alerts">
|
tooltip="Send additional notifications for triggered alerts">
|
||||||
</gf-form-switch>
|
</gf-form-switch>
|
||||||
|
@ -22,7 +22,6 @@ import './export_data/export_data_modal';
|
|||||||
import './ad_hoc_filters';
|
import './ad_hoc_filters';
|
||||||
import './repeat_option/repeat_option';
|
import './repeat_option/repeat_option';
|
||||||
import './dashgrid/DashboardGridDirective';
|
import './dashgrid/DashboardGridDirective';
|
||||||
import './dashgrid/PanelLoader';
|
|
||||||
import './dashgrid/RowOptions';
|
import './dashgrid/RowOptions';
|
||||||
import './folder_picker/folder_picker';
|
import './folder_picker/folder_picker';
|
||||||
import './move_to_folder_modal/move_to_folder';
|
import './move_to_folder_modal/move_to_folder';
|
||||||
|
@ -1,11 +1,10 @@
|
|||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { PanelContainer } from './dashgrid/PanelContainer';
|
|
||||||
import { DashboardModel } from './dashboard_model';
|
import { DashboardModel } from './dashboard_model';
|
||||||
import { PanelModel } from './panel_model';
|
import { PanelModel } from './panel_model';
|
||||||
|
|
||||||
export class DashboardCtrl implements PanelContainer {
|
export class DashboardCtrl {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
dashboardViewState: any;
|
dashboardViewState: any;
|
||||||
loadedFallbackDashboard: boolean;
|
loadedFallbackDashboard: boolean;
|
||||||
@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
private dashboardSrv,
|
private dashboardSrv,
|
||||||
private unsavedChangesSrv,
|
private unsavedChangesSrv,
|
||||||
private dashboardViewStateSrv,
|
private dashboardViewStateSrv,
|
||||||
public playlistSrv,
|
public playlistSrv
|
||||||
private panelLoader
|
|
||||||
) {
|
) {
|
||||||
// temp hack due to way dashboards are loaded
|
// temp hack due to way dashboards are loaded
|
||||||
// can't use controllerAs on route yet
|
// can't use controllerAs on route yet
|
||||||
@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
return this.dashboard;
|
return this.dashboard;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelLoader() {
|
|
||||||
return this.panelLoader;
|
|
||||||
}
|
|
||||||
|
|
||||||
timezoneChanged() {
|
|
||||||
this.$rootScope.$broadcast('refresh');
|
|
||||||
}
|
|
||||||
|
|
||||||
getPanelContainer() {
|
getPanelContainer() {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer {
|
|||||||
this.dashboard.removePanel(panel);
|
this.dashboard.removePanel(panel);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onDestroy() {
|
||||||
|
if (this.dashboard) {
|
||||||
|
this.dashboard.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init(dashboard) {
|
init(dashboard) {
|
||||||
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this));
|
||||||
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this));
|
||||||
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this));
|
||||||
|
this.$scope.$on('$destroy', this.onDestroy.bind(this));
|
||||||
this.setupDashboard(dashboard);
|
this.setupDashboard(dashboard);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,6 +200,43 @@ export class DashboardModel {
|
|||||||
this.events.emit('view-mode-changed', panel);
|
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) {
|
private ensureListExist(data) {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
data = {};
|
data = {};
|
||||||
|
@ -3,7 +3,7 @@ import _ from 'lodash';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
import ScrollBar from 'app/core/components/ScrollBar/ScrollBar';
|
||||||
import store from 'app/core/store';
|
import store from 'app/core/store';
|
||||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||||
@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words';
|
|||||||
|
|
||||||
export interface AddPanelPanelProps {
|
export interface AddPanelPanelProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddPanelPanelState {
|
export interface AddPanelPanelState {
|
||||||
@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
}
|
}
|
||||||
|
|
||||||
onAddPanel = panelPluginInfo => {
|
onAddPanel = panelPluginInfo => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
const dashboard = this.props.dashboard;
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
const { gridPos } = this.props.panel;
|
const { gridPos } = this.props.panel;
|
||||||
|
|
||||||
const newPanel: any = {
|
const newPanel: any = {
|
||||||
@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
|
|||||||
|
|
||||||
handleCloseAddPanel(evt) {
|
handleCloseAddPanel(evt) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removePanel(this.props.dashboard.panels[0]);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
dashboard.removePanel(dashboard.panels[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
renderText(text: string) {
|
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 { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants';
|
||||||
import { DashboardPanel } from './DashboardPanel';
|
import { DashboardPanel } from './DashboardPanel';
|
||||||
import { DashboardModel } from '../dashboard_model';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import sizeMe from 'react-sizeme';
|
import sizeMe from 'react-sizeme';
|
||||||
@ -60,18 +59,15 @@ function GridWrapper({
|
|||||||
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper);
|
||||||
|
|
||||||
export interface DashboardGridProps {
|
export interface DashboardGridProps {
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
||||||
gridToPanelMap: any;
|
gridToPanelMap: any;
|
||||||
panelContainer: PanelContainer;
|
|
||||||
dashboard: DashboardModel;
|
|
||||||
panelMap: { [id: string]: PanelModel };
|
panelMap: { [id: string]: PanelModel };
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.panelContainer = this.props.getPanelContainer();
|
|
||||||
this.onLayoutChange = this.onLayoutChange.bind(this);
|
this.onLayoutChange = this.onLayoutChange.bind(this);
|
||||||
this.onResize = this.onResize.bind(this);
|
this.onResize = this.onResize.bind(this);
|
||||||
this.onResizeStop = this.onResizeStop.bind(this);
|
this.onResizeStop = this.onResizeStop.bind(this);
|
||||||
@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
this.state = { animated: false };
|
this.state = { animated: false };
|
||||||
|
|
||||||
// subscribe to dashboard events
|
// subscribe to dashboard events
|
||||||
this.dashboard = this.panelContainer.getDashboard();
|
const dashboard = this.props.dashboard;
|
||||||
this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
dashboard.on('panel-added', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
dashboard.on('panel-removed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this));
|
dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this));
|
||||||
this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this));
|
||||||
this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
dashboard.on('row-expanded', this.triggerForceUpdate.bind(this));
|
||||||
|
dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
buildLayout() {
|
buildLayout() {
|
||||||
const layout = [];
|
const layout = [];
|
||||||
this.panelMap = {};
|
this.panelMap = {};
|
||||||
|
|
||||||
for (const panel of this.dashboard.panels) {
|
for (const panel of this.props.dashboard.panels) {
|
||||||
const stringId = panel.id.toString();
|
const stringId = panel.id.toString();
|
||||||
this.panelMap[stringId] = panel;
|
this.panelMap[stringId] = panel;
|
||||||
|
|
||||||
@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
this.panelMap[newPos.i].updateGridPos(newPos);
|
this.panelMap[newPos.i].updateGridPos(newPos);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dashboard.sortPanelsByGridPos();
|
this.props.dashboard.sortPanelsByGridPos();
|
||||||
}
|
}
|
||||||
|
|
||||||
triggerForceUpdate() {
|
triggerForceUpdate() {
|
||||||
@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onWidthChange() {
|
onWidthChange() {
|
||||||
for (const panel of this.dashboard.panels) {
|
for (const panel of this.props.dashboard.panels) {
|
||||||
panel.resizeDone();
|
panel.resizeDone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onViewModeChanged(payload) {
|
||||||
|
this.setState({ animated: !payload.fullscreen });
|
||||||
|
}
|
||||||
|
|
||||||
updateGridPos(item, layout) {
|
updateGridPos(item, layout) {
|
||||||
this.panelMap[item.i].updateGridPos(item);
|
this.panelMap[item.i].updateGridPos(item);
|
||||||
|
|
||||||
@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.setState(() => {
|
this.setState({ animated: true });
|
||||||
return { animated: true };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
renderPanels() {
|
renderPanels() {
|
||||||
const panelElements = [];
|
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 });
|
const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen });
|
||||||
panelElements.push(
|
panelElements.push(
|
||||||
/** panel-id is set for html bookmarks */
|
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id}`}>
|
||||||
<div key={panel.id.toString()} className={panelClasses} id={`panel-${panel.id.toString()}`}>
|
<DashboardPanel panel={panel} dashboard={this.props.dashboard} panelType={panel.type} />
|
||||||
<DashboardPanel panel={panel} getPanelContainer={this.props.getPanelContainer} />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component<DashboardGridProps, any> {
|
|||||||
<SizedReactLayoutGrid
|
<SizedReactLayoutGrid
|
||||||
className={classNames({ layout: true, animated: this.state.animated })}
|
className={classNames({ layout: true, animated: this.state.animated })}
|
||||||
layout={this.buildLayout()}
|
layout={this.buildLayout()}
|
||||||
isResizable={this.dashboard.meta.canEdit}
|
isResizable={this.props.dashboard.meta.canEdit}
|
||||||
isDraggable={this.dashboard.meta.canEdit}
|
isDraggable={this.props.dashboard.meta.canEdit}
|
||||||
onLayoutChange={this.onLayoutChange}
|
onLayoutChange={this.onLayoutChange}
|
||||||
onWidthChange={this.onWidthChange}
|
onWidthChange={this.onWidthChange}
|
||||||
onDragStop={this.onDragStop}
|
onDragStop={this.onDragStop}
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||||
import { DashboardGrid } from './DashboardGrid';
|
import { DashboardGrid } from './DashboardGrid';
|
||||||
|
|
||||||
react2AngularDirective('dashboardGrid', DashboardGrid, [
|
react2AngularDirective('dashboardGrid', DashboardGrid, [['dashboard', { watchDepth: 'reference' }]]);
|
||||||
['getPanelContainer', { watchDepth: 'reference', wrapApply: false }],
|
|
||||||
]);
|
|
||||||
|
@ -1,54 +1,161 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {PanelModel} from '../panel_model';
|
import config from 'app/core/config';
|
||||||
import {PanelContainer} from './PanelContainer';
|
import { PanelModel } from '../panel_model';
|
||||||
import {AttachedPanel} from './PanelLoader';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import {DashboardRow} from './DashboardRow';
|
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||||
import {AddPanelPanel} from './AddPanelPanel';
|
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;
|
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;
|
element: any;
|
||||||
attachedPanel: AttachedPanel;
|
angularPanel: AngularComponent;
|
||||||
|
pluginInfo: any;
|
||||||
|
specialPanels = {};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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() {
|
isSpecial() {
|
||||||
if (!this.element) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const panelContainer = this.props.getPanelContainer();
|
// handle plugin loading & changing of plugin type
|
||||||
const dashboard = panelContainer.getDashboard();
|
if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) {
|
||||||
const loader = panelContainer.getPanelLoader();
|
this.pluginInfo = config.panels[this.props.panel.type];
|
||||||
this.attachedPanel = loader.load(this.element, this.props.panel, dashboard);
|
|
||||||
|
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() {
|
componentWillUnmount() {
|
||||||
if (this.attachedPanel) {
|
this.cleanUpAngularPanel();
|
||||||
this.attachedPanel.destroy();
|
}
|
||||||
}
|
|
||||||
|
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() {
|
render() {
|
||||||
// special handling for rows
|
if (this.isSpecial()) {
|
||||||
if (this.props.panel.type === 'row') {
|
return this.specialPanels[this.props.panel.type]();
|
||||||
return <DashboardRow panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.panel.type === 'add-panel') {
|
if (!this.state.pluginExports) {
|
||||||
return <AddPanelPanel panel={this.props.panel} getPanelContainer={this.props.getPanelContainer} />;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
if (this.state.pluginExports.PanelComponent) {
|
||||||
<div ref={element => this.element = element} className="panel-height-helper" />
|
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 React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
import { PanelContainer } from './PanelContainer';
|
import { DashboardModel } from '../dashboard_model';
|
||||||
import templateSrv from 'app/features/templating/template_srv';
|
import templateSrv from 'app/features/templating/template_srv';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
export interface DashboardRowProps {
|
export interface DashboardRowProps {
|
||||||
panel: PanelModel;
|
panel: PanelModel;
|
||||||
getPanelContainer: () => PanelContainer;
|
dashboard: DashboardModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||||
dashboard: any;
|
|
||||||
panelContainer: any;
|
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
@ -21,9 +18,6 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
collapsed: this.props.panel.collapsed,
|
collapsed: this.props.panel.collapsed,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.panelContainer = this.props.getPanelContainer();
|
|
||||||
this.dashboard = this.panelContainer.getDashboard();
|
|
||||||
|
|
||||||
this.toggle = this.toggle.bind(this);
|
this.toggle = this.toggle.bind(this);
|
||||||
this.openSettings = this.openSettings.bind(this);
|
this.openSettings = this.openSettings.bind(this);
|
||||||
this.delete = this.delete.bind(this);
|
this.delete = this.delete.bind(this);
|
||||||
@ -31,7 +25,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.dashboard.toggleRow(this.props.panel);
|
this.props.dashboard.toggleRow(this.props.panel);
|
||||||
|
|
||||||
this.setState(prevState => {
|
this.setState(prevState => {
|
||||||
return { collapsed: !prevState.collapsed };
|
return { collapsed: !prevState.collapsed };
|
||||||
@ -39,7 +33,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update() {
|
update() {
|
||||||
this.dashboard.processRepeats();
|
this.props.dashboard.processRepeats();
|
||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,14 +55,10 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
|||||||
altActionText: 'Delete row only',
|
altActionText: 'Delete row only',
|
||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removeRow(this.props.panel, true);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
dashboard.removeRow(this.props.panel, true);
|
|
||||||
},
|
},
|
||||||
onAltAction: () => {
|
onAltAction: () => {
|
||||||
const panelContainer = this.props.getPanelContainer();
|
this.props.dashboard.removeRow(this.props.panel, false);
|
||||||
const dashboard = panelContainer.getDashboard();
|
|
||||||
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 title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
|
||||||
const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
|
||||||
const panels = count === 1 ? 'panel' : 'panels';
|
const panels = count === 1 ? 'panel' : 'panels';
|
||||||
const canEdit = this.dashboard.meta.canEdit === true;
|
const canEdit = this.props.dashboard.meta.canEdit === true;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classes}>
|
<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) {
|
} else if (search.fullscreen) {
|
||||||
delete search.fullscreen;
|
delete search.fullscreen;
|
||||||
delete search.edit;
|
delete search.edit;
|
||||||
|
delete search.tab;
|
||||||
|
delete search.panelId;
|
||||||
}
|
}
|
||||||
this.$location.search(search);
|
this.$location.search(search);
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = {
|
|||||||
events: true,
|
events: true,
|
||||||
fullscreen: true,
|
fullscreen: true,
|
||||||
isEditing: true,
|
isEditing: true,
|
||||||
|
hasRefreshed: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaults: any = {
|
||||||
|
gridPos: { x: 0, y: 0, h: 3, w: 6 },
|
||||||
|
datasource: null,
|
||||||
|
targets: [{}],
|
||||||
};
|
};
|
||||||
|
|
||||||
export class PanelModel {
|
export class PanelModel {
|
||||||
@ -31,10 +38,14 @@ export class PanelModel {
|
|||||||
collapsed?: boolean;
|
collapsed?: boolean;
|
||||||
panels?: any;
|
panels?: any;
|
||||||
soloMode?: boolean;
|
soloMode?: boolean;
|
||||||
|
targets: any[];
|
||||||
|
datasource: string;
|
||||||
|
thresholds?: any;
|
||||||
|
|
||||||
// non persisted
|
// non persisted
|
||||||
fullscreen: boolean;
|
fullscreen: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
|
hasRefreshed: boolean;
|
||||||
events: Emitter;
|
events: Emitter;
|
||||||
|
|
||||||
constructor(model) {
|
constructor(model) {
|
||||||
@ -45,9 +56,8 @@ export class PanelModel {
|
|||||||
this[property] = model[property];
|
this[property] = model[property];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.gridPos) {
|
// defaults
|
||||||
this.gridPos = { x: 0, y: 0, h: 3, w: 6 };
|
_.defaultsDeep(this, _.cloneDeep(defaults));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getSaveModel() {
|
getSaveModel() {
|
||||||
@ -57,6 +67,10 @@ export class PanelModel {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_.isEqual(this[property], defaults[property])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
model[property] = _.cloneDeep(this[property]);
|
model[property] = _.cloneDeep(this[property]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,7 +96,6 @@ export class PanelModel {
|
|||||||
this.gridPos.h = newPos.h;
|
this.gridPos.h = newPos.h;
|
||||||
|
|
||||||
if (sizeChanged) {
|
if (sizeChanged) {
|
||||||
console.log('PanelModel sizeChanged event and render events fired');
|
|
||||||
this.events.emit('panel-size-changed');
|
this.events.emit('panel-size-changed');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,6 +104,34 @@ export class PanelModel {
|
|||||||
this.events.emit('panel-size-changed');
|
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() {
|
destroy() {
|
||||||
this.events.removeAllListeners();
|
this.events.removeAllListeners();
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ export class SettingsCtrl {
|
|||||||
|
|
||||||
this.$scope.$on('$destroy', () => {
|
this.$scope.$on('$destroy', () => {
|
||||||
this.dashboard.updateSubmenuVisibility();
|
this.dashboard.updateSubmenuVisibility();
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.startRefresh();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
this.$rootScope.appEvent('dash-scroll', { restore: true });
|
||||||
});
|
});
|
||||||
|
@ -46,8 +46,7 @@ export class ShareSnapshotCtrl {
|
|||||||
|
|
||||||
$scope.loading = true;
|
$scope.loading = true;
|
||||||
$scope.snapshot.external = external;
|
$scope.snapshot.external = external;
|
||||||
|
$scope.dashboard.startRefresh();
|
||||||
$rootScope.$broadcast('refresh');
|
|
||||||
|
|
||||||
$timeout(() => {
|
$timeout(() => {
|
||||||
$scope.saveSnapshot(external);
|
$scope.saveSnapshot(external);
|
||||||
|
@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('AddPanelPanel', () => {
|
describe('AddPanelPanel', () => {
|
||||||
let wrapper, dashboardMock, getPanelContainer, panel;
|
let wrapper, dashboardMock, panel;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
config.panels = [
|
config.panels = [
|
||||||
@ -23,6 +23,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Singlestat',
|
name: 'Singlestat',
|
||||||
sort: 2,
|
sort: 2,
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
|
meta: {},
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -34,6 +37,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: true,
|
hideFromList: true,
|
||||||
name: 'Hidden',
|
name: 'Hidden',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -45,6 +51,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Graph',
|
name: 'Graph',
|
||||||
sort: 1,
|
sort: 1,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -56,6 +65,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Zabbix',
|
name: 'Zabbix',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -67,6 +79,9 @@ describe('AddPanelPanel', () => {
|
|||||||
hideFromList: false,
|
hideFromList: false,
|
||||||
name: 'Piechart',
|
name: 'Piechart',
|
||||||
sort: 100,
|
sort: 100,
|
||||||
|
meta: {},
|
||||||
|
module: '',
|
||||||
|
baseUrl: '',
|
||||||
info: {
|
info: {
|
||||||
logos: {
|
logos: {
|
||||||
small: '',
|
small: '',
|
||||||
@ -77,13 +92,8 @@ describe('AddPanelPanel', () => {
|
|||||||
|
|
||||||
dashboardMock = { toggleRow: jest.fn() };
|
dashboardMock = { toggleRow: jest.fn() };
|
||||||
|
|
||||||
getPanelContainer = jest.fn().mockReturnValue({
|
|
||||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
|
||||||
getPanelLoader: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<AddPanelPanel panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<AddPanelPanel panel={panel} dashboard={dashboardMock} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should fetch all panels sorted with core plugins first', () => {
|
it('should fetch all panels sorted with core plugins first', () => {
|
||||||
|
@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow';
|
|||||||
import { PanelModel } from '../panel_model';
|
import { PanelModel } from '../panel_model';
|
||||||
|
|
||||||
describe('DashboardRow', () => {
|
describe('DashboardRow', () => {
|
||||||
let wrapper, panel, getPanelContainer, dashboardMock;
|
let wrapper, panel, dashboardMock;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
dashboardMock = {
|
dashboardMock = {
|
||||||
@ -14,13 +14,8 @@ describe('DashboardRow', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
getPanelContainer = jest.fn().mockReturnValue({
|
|
||||||
getDashboard: jest.fn().mockReturnValue(dashboardMock),
|
|
||||||
getPanelLoader: jest.fn(),
|
|
||||||
});
|
|
||||||
|
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Should not have collapsed class when collaped is false', () => {
|
it('Should not have collapsed class when collaped is false', () => {
|
||||||
@ -41,14 +36,14 @@ describe('DashboardRow', () => {
|
|||||||
|
|
||||||
it('should not show row drag handle when cannot edit', () => {
|
it('should not show row drag handle when cannot edit', () => {
|
||||||
dashboardMock.meta.canEdit = false;
|
dashboardMock.meta.canEdit = false;
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
|
expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have zero actions when cannot edit', () => {
|
it('should have zero actions when cannot edit', () => {
|
||||||
dashboardMock.meta.canEdit = false;
|
dashboardMock.meta.canEdit = false;
|
||||||
panel = new PanelModel({ collapsed: false });
|
panel = new PanelModel({ collapsed: false });
|
||||||
wrapper = shallow(<DashboardRow panel={panel} getPanelContainer={getPanelContainer} />);
|
wrapper = shallow(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||||
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -240,5 +240,5 @@ stubs['-- Grafana --'] = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
function getStub(arg) {
|
function getStub(arg) {
|
||||||
return Promise.resolve(stubs[arg]);
|
return Promise.resolve(stubs[arg || 'gfdb']);
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import 'app/features/dashboard/view_state_srv';
|
import 'app/features/dashboard/view_state_srv';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { DashboardViewState } from '../view_state_srv';
|
import { DashboardViewState } from '../view_state_srv';
|
||||||
|
import { DashboardModel } from '../dashboard_model';
|
||||||
|
|
||||||
describe('when updating view state', () => {
|
describe('when updating view state', () => {
|
||||||
const location = {
|
const location = {
|
||||||
@ -10,14 +11,13 @@ describe('when updating view state', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const $scope = {
|
const $scope = {
|
||||||
|
appEvent: jest.fn(),
|
||||||
onAppEvent: jest.fn(() => {}),
|
onAppEvent: jest.fn(() => {}),
|
||||||
dashboard: {
|
dashboard: new DashboardModel({
|
||||||
meta: {},
|
panels: [{ id: 1 }],
|
||||||
panels: [],
|
}),
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const $rootScope = {};
|
|
||||||
let viewState;
|
let viewState;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@ -33,7 +33,7 @@ describe('when updating view state', () => {
|
|||||||
location.search = jest.fn(() => {
|
location.search = jest.fn(() => {
|
||||||
return { fullscreen: true, edit: true, panelId: 1 };
|
return { fullscreen: true, edit: true, panelId: 1 };
|
||||||
});
|
});
|
||||||
viewState = new DashboardViewState($scope, location, {}, $rootScope);
|
viewState = new DashboardViewState($scope, location, {});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update querystring and view state', () => {
|
it('should update querystring and view state', () => {
|
||||||
@ -55,7 +55,7 @@ describe('when updating view state', () => {
|
|||||||
|
|
||||||
describe('to fullscreen false', () => {
|
describe('to fullscreen false', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewState = new DashboardViewState($scope, location, {}, $rootScope);
|
viewState = new DashboardViewState($scope, location, {});
|
||||||
});
|
});
|
||||||
it('should remove params from query string', () => {
|
it('should remove params from query string', () => {
|
||||||
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
viewState.update({ fullscreen: true, panelId: 1, edit: true });
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { StoreState } from 'app/types';
|
import { StoreState } from 'app/types';
|
||||||
import { ThunkAction } from 'redux-thunk';
|
import { ThunkAction } from 'redux-thunk';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||||
import {
|
import {
|
||||||
DashboardAcl,
|
DashboardAcl,
|
||||||
DashboardAclDTO,
|
DashboardAclDTO,
|
||||||
@ -113,3 +114,18 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar
|
|||||||
await dispatch(getDashboardPermissions(dashboardId));
|
await dispatch(getDashboardPermissions(dashboardId));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function importDashboard(data, dashboardTitle: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().post('/api/dashboards/import', data);
|
||||||
|
appEvents.emit('alert-success', ['Dashboard Imported', dashboardTitle]);
|
||||||
|
dispatch(loadPluginDashboards());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeDashboard(uri: string): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
await getBackendSrv().delete(`/api/dashboards/${uri}`);
|
||||||
|
dispatch(loadPluginDashboards());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -7,13 +7,13 @@ export class SubmenuCtrl {
|
|||||||
dashboard: any;
|
dashboard: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private $rootScope, private variableSrv, private $location) {
|
constructor(private variableSrv, private $location) {
|
||||||
this.annotations = this.dashboard.templating.list;
|
this.annotations = this.dashboard.templating.list;
|
||||||
this.variables = this.variableSrv.variables;
|
this.variables = this.variableSrv.variables;
|
||||||
}
|
}
|
||||||
|
|
||||||
annotationStateChanged() {
|
annotationStateChanged() {
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.startRefresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
variableUpdated(variable) {
|
variableUpdated(variable) {
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
|
// Libraries
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
|
||||||
|
// Utils
|
||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
import * as dateMath from 'app/core/utils/datemath';
|
import * as dateMath from 'app/core/utils/datemath';
|
||||||
|
// Types
|
||||||
|
|
||||||
|
import { TimeRange } from 'app/types';
|
||||||
|
|
||||||
export class TimeSrv {
|
export class TimeSrv {
|
||||||
time: any;
|
time: any;
|
||||||
@ -24,7 +30,6 @@ export class TimeSrv {
|
|||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
|
if (this.autoRefreshBlocked && document.visibilityState === 'visible') {
|
||||||
this.autoRefreshBlocked = false;
|
this.autoRefreshBlocked = false;
|
||||||
|
|
||||||
this.refreshDashboard();
|
this.refreshDashboard();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -142,7 +147,7 @@ export class TimeSrv {
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshDashboard() {
|
refreshDashboard() {
|
||||||
this.$rootScope.$broadcast('refresh');
|
this.dashboard.timeRangeUpdated();
|
||||||
}
|
}
|
||||||
|
|
||||||
private startNextRefreshTimer(afterMs) {
|
private startNextRefreshTimer(afterMs) {
|
||||||
@ -201,7 +206,7 @@ export class TimeSrv {
|
|||||||
return range;
|
return range;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRange() {
|
timeRange(): TimeRange {
|
||||||
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
// make copies if they are moment (do not want to return out internal moment, because they are mutable!)
|
||||||
const raw = {
|
const raw = {
|
||||||
from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
|
from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from,
|
||||||
@ -223,17 +228,21 @@ export class TimeSrv {
|
|||||||
const timespan = range.to.valueOf() - range.from.valueOf();
|
const timespan = range.to.valueOf() - range.from.valueOf();
|
||||||
const center = range.to.valueOf() - timespan / 2;
|
const center = range.to.valueOf() - timespan / 2;
|
||||||
|
|
||||||
let to = center + timespan * factor / 2;
|
const to = center + timespan * factor / 2;
|
||||||
let from = center - timespan * factor / 2;
|
const from = center - timespan * factor / 2;
|
||||||
|
|
||||||
if (to > Date.now() && range.to <= Date.now()) {
|
|
||||||
const offset = to - Date.now();
|
|
||||||
from = from - offset;
|
|
||||||
to = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
this.setTime({ from: moment.utc(from), to: moment.utc(to) });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let singleton;
|
||||||
|
|
||||||
|
export function setTimeSrv(srv: TimeSrv) {
|
||||||
|
singleton = srv;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTimeSrv(): TimeSrv {
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
coreModule.service('timeSrv', TimeSrv);
|
coreModule.service('timeSrv', TimeSrv);
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label width-10">Timezone</label>
|
<label class="gf-form-label width-10">Timezone</label>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]" ng-change="timezoneChanged()"></select>
|
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -31,9 +31,10 @@ export class TimePickerCtrl {
|
|||||||
|
|
||||||
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
$rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope);
|
||||||
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
$rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope);
|
||||||
$rootScope.onAppEvent('refresh', this.onRefresh.bind(this), $scope);
|
|
||||||
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
|
$rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope);
|
||||||
|
|
||||||
|
this.dashboard.on('refresh', this.onRefresh.bind(this), $scope);
|
||||||
|
|
||||||
// init options
|
// init options
|
||||||
this.panel = this.dashboard.timepicker;
|
this.panel = this.dashboard.timepicker;
|
||||||
_.defaults(this.panel, TimePickerCtrl.defaults);
|
_.defaults(this.panel, TimePickerCtrl.defaults);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
import { DashboardModel } from './dashboard_model';
|
import { DashboardModel } from './dashboard_model';
|
||||||
|
|
||||||
// represents the transient view state
|
// represents the transient view state
|
||||||
@ -10,12 +11,11 @@ export class DashboardViewState {
|
|||||||
panelScopes: any;
|
panelScopes: any;
|
||||||
$scope: any;
|
$scope: any;
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
editStateChanged: any;
|
|
||||||
fullscreenPanel: any;
|
fullscreenPanel: any;
|
||||||
oldTimeRange: any;
|
oldTimeRange: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor($scope, private $location, private $timeout, private $rootScope) {
|
constructor($scope, private $location, private $timeout) {
|
||||||
const self = this;
|
const self = this;
|
||||||
self.state = {};
|
self.state = {};
|
||||||
self.panelScopes = [];
|
self.panelScopes = [];
|
||||||
@ -33,10 +33,6 @@ export class DashboardViewState {
|
|||||||
self.update(payload);
|
self.update(payload);
|
||||||
});
|
});
|
||||||
|
|
||||||
$scope.onAppEvent('panel-initialized', (evt, payload) => {
|
|
||||||
self.registerPanel(payload.scope);
|
|
||||||
});
|
|
||||||
|
|
||||||
// this marks changes to location during this digest cycle as not to add history item
|
// this marks changes to location during this digest cycle as not to add history item
|
||||||
// don't want url changes like adding orgId to add browser history
|
// don't want url changes like adding orgId to add browser history
|
||||||
$location.replace();
|
$location.replace();
|
||||||
@ -75,9 +71,6 @@ export class DashboardViewState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// remember if editStateChanged
|
|
||||||
this.editStateChanged = (state.edit || false) !== (this.state.edit || false);
|
|
||||||
|
|
||||||
_.extend(this.state, state);
|
_.extend(this.state, state);
|
||||||
this.dashboard.meta.fullscreen = this.state.fullscreen;
|
this.dashboard.meta.fullscreen = this.state.fullscreen;
|
||||||
|
|
||||||
@ -124,110 +117,59 @@ export class DashboardViewState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
syncState() {
|
syncState() {
|
||||||
if (this.panelScopes.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.dashboard.meta.fullscreen) {
|
if (this.dashboard.meta.fullscreen) {
|
||||||
const panelScope = this.getPanelScope(this.state.panelId);
|
const panel = this.dashboard.getPanelById(this.state.panelId);
|
||||||
if (!panelScope) {
|
|
||||||
|
if (!panel) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.fullscreenPanel) {
|
if (!panel.fullscreen) {
|
||||||
// if already fullscreen
|
this.enterFullscreen(panel);
|
||||||
if (this.fullscreenPanel === panelScope && this.editStateChanged === false) {
|
} else {
|
||||||
return;
|
// already in fullscreen view just update the view mode
|
||||||
} else {
|
this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit);
|
||||||
this.leaveFullscreen(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!panelScope.ctrl.editModeInitiated) {
|
|
||||||
panelScope.ctrl.initEditMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!panelScope.ctrl.fullscreen) {
|
|
||||||
this.enterFullscreen(panelScope);
|
|
||||||
}
|
}
|
||||||
} else if (this.fullscreenPanel) {
|
} else if (this.fullscreenPanel) {
|
||||||
this.leaveFullscreen(true);
|
this.leaveFullscreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getPanelScope(id) {
|
leaveFullscreen() {
|
||||||
return _.find(this.panelScopes, panelScope => {
|
const panel = this.fullscreenPanel;
|
||||||
return panelScope.ctrl.panel.id === id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
leaveFullscreen(render) {
|
this.dashboard.setViewMode(panel, false, false);
|
||||||
const self = this;
|
|
||||||
const ctrl = self.fullscreenPanel.ctrl;
|
|
||||||
|
|
||||||
ctrl.editMode = false;
|
delete this.fullscreenPanel;
|
||||||
ctrl.fullscreen = false;
|
|
||||||
|
|
||||||
this.dashboard.setViewMode(ctrl.panel, false, false);
|
|
||||||
this.$scope.appEvent('panel-fullscreen-exit', { panelId: ctrl.panel.id });
|
|
||||||
this.$scope.appEvent('dash-scroll', { restore: true });
|
|
||||||
|
|
||||||
if (!render) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$timeout(() => {
|
this.$timeout(() => {
|
||||||
if (self.oldTimeRange !== ctrl.range) {
|
appEvents.emit('dash-scroll', { restore: true });
|
||||||
self.$rootScope.$broadcast('refresh');
|
|
||||||
|
if (this.oldTimeRange !== this.dashboard.time) {
|
||||||
|
this.dashboard.startRefresh();
|
||||||
} else {
|
} else {
|
||||||
self.$rootScope.$broadcast('render');
|
this.dashboard.render();
|
||||||
}
|
}
|
||||||
delete self.fullscreenPanel;
|
|
||||||
});
|
});
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enterFullscreen(panelScope) {
|
enterFullscreen(panel) {
|
||||||
const ctrl = panelScope.ctrl;
|
const isEditing = this.state.edit && this.dashboard.meta.canEdit;
|
||||||
|
|
||||||
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
|
this.oldTimeRange = this.dashboard.time;
|
||||||
ctrl.fullscreen = true;
|
this.fullscreenPanel = panel;
|
||||||
|
|
||||||
this.oldTimeRange = ctrl.range;
|
|
||||||
this.fullscreenPanel = panelScope;
|
|
||||||
|
|
||||||
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
|
// Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode()
|
||||||
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 });
|
||||||
this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode);
|
this.dashboard.setViewMode(panel, true, isEditing);
|
||||||
this.$scope.appEvent('panel-fullscreen-enter', { panelId: ctrl.panel.id });
|
|
||||||
}
|
|
||||||
|
|
||||||
registerPanel(panelScope) {
|
|
||||||
const self = this;
|
|
||||||
self.panelScopes.push(panelScope);
|
|
||||||
|
|
||||||
if (!self.dashboard.meta.soloMode) {
|
|
||||||
if (self.state.panelId === panelScope.ctrl.panel.id) {
|
|
||||||
if (self.state.edit) {
|
|
||||||
panelScope.ctrl.editPanel();
|
|
||||||
} else {
|
|
||||||
panelScope.ctrl.viewPanel();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const unbind = panelScope.$on('$destroy', () => {
|
|
||||||
self.panelScopes = _.without(self.panelScopes, panelScope);
|
|
||||||
unbind();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function dashboardViewStateSrv($location, $timeout, $rootScope) {
|
export function dashboardViewStateSrv($location, $timeout) {
|
||||||
return {
|
return {
|
||||||
create: $scope => {
|
create: $scope => {
|
||||||
return new DashboardViewState($scope, $location, $timeout, $rootScope);
|
return new DashboardViewState($scope, $location, $timeout);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
63
public/app/features/datasources/DashboardsTable.test.tsx
Normal file
63
public/app/features/datasources/DashboardsTable.test.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import DashboardsTable, { Props } from './DashboardsTable';
|
||||||
|
import { PluginDashboard } from '../../types';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
dashboards: [] as PluginDashboard[],
|
||||||
|
onImport: jest.fn(),
|
||||||
|
onRemove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return shallow(<DashboardsTable {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render table', () => {
|
||||||
|
const wrapper = setup({
|
||||||
|
dashboards: [
|
||||||
|
{
|
||||||
|
dashboardId: 0,
|
||||||
|
description: '',
|
||||||
|
folderId: 0,
|
||||||
|
imported: false,
|
||||||
|
importedRevision: 0,
|
||||||
|
importedUri: '',
|
||||||
|
importedUrl: '',
|
||||||
|
path: 'dashboards/carbon_metrics.json',
|
||||||
|
pluginId: 'graphite',
|
||||||
|
removed: false,
|
||||||
|
revision: 1,
|
||||||
|
slug: '',
|
||||||
|
title: 'Graphite Carbon Metrics',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dashboardId: 0,
|
||||||
|
description: '',
|
||||||
|
folderId: 0,
|
||||||
|
imported: true,
|
||||||
|
importedRevision: 0,
|
||||||
|
importedUri: '',
|
||||||
|
importedUrl: '',
|
||||||
|
path: 'dashboards/carbon_metrics.json',
|
||||||
|
pluginId: 'graphite',
|
||||||
|
removed: false,
|
||||||
|
revision: 1,
|
||||||
|
slug: '',
|
||||||
|
title: 'Graphite Carbon Metrics',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
55
public/app/features/datasources/DashboardsTable.tsx
Normal file
55
public/app/features/datasources/DashboardsTable.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import React, { SFC } from 'react';
|
||||||
|
import { PluginDashboard } from '../../types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
dashboards: PluginDashboard[];
|
||||||
|
onImport: (dashboard, overwrite) => void;
|
||||||
|
onRemove: (dashboard) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DashboardsTable: SFC<Props> = ({ dashboards, onImport, onRemove }) => {
|
||||||
|
function buttonText(dashboard: PluginDashboard) {
|
||||||
|
return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="filter-table">
|
||||||
|
<tbody>
|
||||||
|
{dashboards.map((dashboard, index) => {
|
||||||
|
return (
|
||||||
|
<tr key={`${dashboard.dashboardId}-${index}`}>
|
||||||
|
<td className="width-1">
|
||||||
|
<i className="icon-gf icon-gf-dashboard" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{dashboard.imported ? (
|
||||||
|
<a href={dashboard.importedUrl}>{dashboard.title}</a>
|
||||||
|
) : (
|
||||||
|
<span>{dashboard.title}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td style={{ textAlign: 'right' }}>
|
||||||
|
{!dashboard.imported ? (
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, false)}>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button className="btn btn-secondary btn-small" onClick={() => onImport(dashboard, true)}>
|
||||||
|
{buttonText(dashboard)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{dashboard.imported && (
|
||||||
|
<button className="btn btn-danger btn-small" onClick={() => onRemove(dashboard)}>
|
||||||
|
<i className="fa fa-trash" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DashboardsTable;
|
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
import { DataSourceDashboards, Props } from './DataSourceDashboards';
|
||||||
|
import { DataSource, NavModel, PluginDashboard } from 'app/types';
|
||||||
|
|
||||||
|
const setup = (propOverrides?: object) => {
|
||||||
|
const props: Props = {
|
||||||
|
navModel: {} as NavModel,
|
||||||
|
dashboards: [] as PluginDashboard[],
|
||||||
|
dataSource: {} as DataSource,
|
||||||
|
pageId: 1,
|
||||||
|
importDashboard: jest.fn(),
|
||||||
|
loadDataSource: jest.fn(),
|
||||||
|
loadPluginDashboards: jest.fn(),
|
||||||
|
removeDashboard: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.assign(props, propOverrides);
|
||||||
|
|
||||||
|
return shallow(<DataSourceDashboards {...props} />);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Render', () => {
|
||||||
|
it('should render component', () => {
|
||||||
|
const wrapper = setup();
|
||||||
|
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
93
public/app/features/datasources/DataSourceDashboards.tsx
Normal file
93
public/app/features/datasources/DataSourceDashboards.tsx
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { hot } from 'react-hot-loader';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
|
import DashboardTable from './DashboardsTable';
|
||||||
|
import { DataSource, NavModel, PluginDashboard } from 'app/types';
|
||||||
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
|
import { getRouteParamsId } from 'app/core/selectors/location';
|
||||||
|
import { loadDataSource } from './state/actions';
|
||||||
|
import { loadPluginDashboards } from '../plugins/state/actions';
|
||||||
|
import { importDashboard, removeDashboard } from '../dashboard/state/actions';
|
||||||
|
import { getDataSource } from './state/selectors';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
navModel: NavModel;
|
||||||
|
dashboards: PluginDashboard[];
|
||||||
|
dataSource: DataSource;
|
||||||
|
pageId: number;
|
||||||
|
importDashboard: typeof importDashboard;
|
||||||
|
loadDataSource: typeof loadDataSource;
|
||||||
|
loadPluginDashboards: typeof loadPluginDashboards;
|
||||||
|
removeDashboard: typeof removeDashboard;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataSourceDashboards extends PureComponent<Props> {
|
||||||
|
async componentDidMount() {
|
||||||
|
const { loadDataSource, pageId } = this.props;
|
||||||
|
|
||||||
|
await loadDataSource(pageId);
|
||||||
|
this.props.loadPluginDashboards();
|
||||||
|
}
|
||||||
|
|
||||||
|
onImport = (dashboard: PluginDashboard, overwrite: boolean) => {
|
||||||
|
const { dataSource, importDashboard } = this.props;
|
||||||
|
const data = {
|
||||||
|
pluginId: dashboard.pluginId,
|
||||||
|
path: dashboard.path,
|
||||||
|
overwrite: overwrite,
|
||||||
|
inputs: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dataSource) {
|
||||||
|
data.inputs.push({
|
||||||
|
name: '*',
|
||||||
|
type: 'datasource',
|
||||||
|
pluginId: dataSource.type,
|
||||||
|
value: dataSource.name,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
importDashboard(data, dashboard.title);
|
||||||
|
};
|
||||||
|
|
||||||
|
onRemove = (dashboard: PluginDashboard) => {
|
||||||
|
this.props.removeDashboard(dashboard.importedUri);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { dashboards, navModel } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader model={navModel} />
|
||||||
|
<div className="page-container page-body">
|
||||||
|
<DashboardTable
|
||||||
|
dashboards={dashboards}
|
||||||
|
onImport={(dashboard, overwrite) => this.onImport(dashboard, overwrite)}
|
||||||
|
onRemove={dashboard => this.onRemove(dashboard)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
const pageId = getRouteParamsId(state.location);
|
||||||
|
|
||||||
|
return {
|
||||||
|
navModel: getNavModel(state.navIndex, `datasource-dashboards-${pageId}`),
|
||||||
|
pageId: pageId,
|
||||||
|
dashboards: state.plugins.dashboards,
|
||||||
|
dataSource: getDataSource(state.dataSources, pageId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapDispatchToProps = {
|
||||||
|
importDashboard,
|
||||||
|
loadDataSource,
|
||||||
|
loadPluginDashboards,
|
||||||
|
removeDashboard,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DataSourceDashboards));
|
125
public/app/features/datasources/DataSourceSettings.tsx
Normal file
125
public/app/features/datasources/DataSourceSettings.tsx
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { DataSource, Plugin } from 'app/types';
|
||||||
|
|
||||||
|
export interface Props {
|
||||||
|
dataSource: DataSource;
|
||||||
|
dataSourceMeta: Plugin;
|
||||||
|
}
|
||||||
|
interface State {
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum DataSourceStates {
|
||||||
|
Alpha = 'alpha',
|
||||||
|
Beta = 'beta',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DataSourceSettings extends PureComponent<Props, State> {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
name: props.dataSource.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
onNameChange = event => {
|
||||||
|
this.setState({
|
||||||
|
name: event.target.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onSubmit = event => {
|
||||||
|
event.preventDefault();
|
||||||
|
console.log(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
onDelete = event => {
|
||||||
|
console.log(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
isReadyOnly() {
|
||||||
|
return this.props.dataSource.readOnly === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldRenderInfoBox() {
|
||||||
|
const { state } = this.props.dataSourceMeta;
|
||||||
|
|
||||||
|
return state === DataSourceStates.Alpha || state === DataSourceStates.Beta;
|
||||||
|
}
|
||||||
|
|
||||||
|
getInfoText() {
|
||||||
|
const { dataSourceMeta } = this.props;
|
||||||
|
|
||||||
|
switch (dataSourceMeta.state) {
|
||||||
|
case DataSourceStates.Alpha:
|
||||||
|
return (
|
||||||
|
'This plugin is marked as being in alpha state, which means it is in early development phase and updates' +
|
||||||
|
' will include breaking changes.'
|
||||||
|
);
|
||||||
|
|
||||||
|
case DataSourceStates.Beta:
|
||||||
|
return (
|
||||||
|
'This plugin is marked as being in a beta development state. This means it is in currently in active' +
|
||||||
|
' development and could be missing important features.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { name } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h3 className="page-sub-heading">Settings</h3>
|
||||||
|
<form onSubmit={this.onSubmit}>
|
||||||
|
<div className="gf-form-group">
|
||||||
|
<div className="gf-form-inline">
|
||||||
|
<div className="gf-form max-width-30">
|
||||||
|
<span className="gf-form-label width-10">Name</span>
|
||||||
|
<input
|
||||||
|
className="gf-form-input max-width-23"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
placeholder="name"
|
||||||
|
onChange={this.onNameChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{this.shouldRenderInfoBox() && <div className="grafana-info-box">{this.getInfoText()}</div>}
|
||||||
|
{this.isReadyOnly() && (
|
||||||
|
<div className="grafana-info-box span8">
|
||||||
|
This datasource was added by config and cannot be modified using the UI. Please contact your server admin
|
||||||
|
to update this datasource.
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form-button-row">
|
||||||
|
<button type="submit" className="btn btn-success" disabled={this.isReadyOnly()} onClick={this.onSubmit}>
|
||||||
|
Save & Test
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-danger" disabled={this.isReadyOnly()} onClick={this.onDelete}>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<a className="btn btn-inverse" href="datasources">
|
||||||
|
Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state) {
|
||||||
|
return {
|
||||||
|
dataSource: state.dataSources.dataSource,
|
||||||
|
dataSourceMeta: state.dataSources.dataSourceMeta,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(mapStateToProps)(DataSourceSettings);
|
@ -4,7 +4,6 @@ import { hot } from 'react-hot-loader';
|
|||||||
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
import PageHeader from 'app/core/components/PageHeader/PageHeader';
|
||||||
import { NavModel, Plugin } from 'app/types';
|
import { NavModel, Plugin } from 'app/types';
|
||||||
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
|
import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
|
||||||
import { updateLocation } from '../../core/actions';
|
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { getDataSourceTypes } from './state/selectors';
|
import { getDataSourceTypes } from './state/selectors';
|
||||||
|
|
||||||
@ -13,7 +12,6 @@ export interface Props {
|
|||||||
dataSourceTypes: Plugin[];
|
dataSourceTypes: Plugin[];
|
||||||
addDataSource: typeof addDataSource;
|
addDataSource: typeof addDataSource;
|
||||||
loadDataSourceTypes: typeof loadDataSourceTypes;
|
loadDataSourceTypes: typeof loadDataSourceTypes;
|
||||||
updateLocation: typeof updateLocation;
|
|
||||||
dataSourceTypeSearchQuery: string;
|
dataSourceTypeSearchQuery: string;
|
||||||
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
|
setDataSourceTypeSearchQuery: typeof setDataSourceTypeSearchQuery;
|
||||||
}
|
}
|
||||||
@ -81,7 +79,6 @@ function mapStateToProps(state) {
|
|||||||
const mapDispatchToProps = {
|
const mapDispatchToProps = {
|
||||||
addDataSource,
|
addDataSource,
|
||||||
loadDataSourceTypes,
|
loadDataSourceTypes,
|
||||||
updateLocation,
|
|
||||||
setDataSourceTypeSearchQuery,
|
setDataSourceTypeSearchQuery,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,88 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<table
|
||||||
|
className="filter-table"
|
||||||
|
>
|
||||||
|
<tbody />
|
||||||
|
</table>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`Render should render table 1`] = `
|
||||||
|
<table
|
||||||
|
className="filter-table"
|
||||||
|
>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
key="0-0"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-1"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-dashboard"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
Graphite Carbon Metrics
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textAlign": "right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary btn-small"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
key="0-1"
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
className="width-1"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="icon-gf icon-gf-dashboard"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
>
|
||||||
|
Graphite Carbon Metrics
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"textAlign": "right",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="btn btn-secondary btn-small"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-danger btn-small"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
className="fa fa-trash"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
`;
|
@ -0,0 +1,18 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Render should render component 1`] = `
|
||||||
|
<div>
|
||||||
|
<PageHeader
|
||||||
|
model={Object {}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="page-container page-body"
|
||||||
|
>
|
||||||
|
<DashboardsTable
|
||||||
|
dashboards={Array []}
|
||||||
|
onImport={[Function]}
|
||||||
|
onRemove={[Function]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
@ -2,12 +2,15 @@ import { ThunkAction } from 'redux-thunk';
|
|||||||
import { DataSource, Plugin, StoreState } from 'app/types';
|
import { DataSource, Plugin, StoreState } from 'app/types';
|
||||||
import { getBackendSrv } from '../../../core/services/backend_srv';
|
import { getBackendSrv } from '../../../core/services/backend_srv';
|
||||||
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector';
|
||||||
import { updateLocation } from '../../../core/actions';
|
import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions';
|
||||||
import { UpdateLocationAction } from '../../../core/actions/location';
|
import { UpdateLocationAction } from '../../../core/actions/location';
|
||||||
|
import { buildNavModel } from './navModel';
|
||||||
|
|
||||||
export enum ActionTypes {
|
export enum ActionTypes {
|
||||||
LoadDataSources = 'LOAD_DATA_SOURCES',
|
LoadDataSources = 'LOAD_DATA_SOURCES',
|
||||||
LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
|
LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES',
|
||||||
|
LoadDataSource = 'LOAD_DATA_SOURCE',
|
||||||
|
LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META',
|
||||||
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
|
SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY',
|
||||||
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
|
SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE',
|
||||||
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
|
SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY',
|
||||||
@ -38,11 +41,31 @@ export interface SetDataSourceTypeSearchQueryAction {
|
|||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LoadDataSourceAction {
|
||||||
|
type: ActionTypes.LoadDataSource;
|
||||||
|
payload: DataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LoadDataSourceMetaAction {
|
||||||
|
type: ActionTypes.LoadDataSourceMeta;
|
||||||
|
payload: Plugin;
|
||||||
|
}
|
||||||
|
|
||||||
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
|
const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({
|
||||||
type: ActionTypes.LoadDataSources,
|
type: ActionTypes.LoadDataSources,
|
||||||
payload: dataSources,
|
payload: dataSources,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dataSourceLoaded = (dataSource: DataSource): LoadDataSourceAction => ({
|
||||||
|
type: ActionTypes.LoadDataSource,
|
||||||
|
payload: dataSource,
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataSourceMetaLoaded = (dataSourceMeta: Plugin): LoadDataSourceMetaAction => ({
|
||||||
|
type: ActionTypes.LoadDataSourceMeta,
|
||||||
|
payload: dataSourceMeta,
|
||||||
|
});
|
||||||
|
|
||||||
const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
|
const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAction => ({
|
||||||
type: ActionTypes.LoadDataSourceTypes,
|
type: ActionTypes.LoadDataSourceTypes,
|
||||||
payload: dataSourceTypes,
|
payload: dataSourceTypes,
|
||||||
@ -69,7 +92,10 @@ export type Action =
|
|||||||
| SetDataSourcesLayoutModeAction
|
| SetDataSourcesLayoutModeAction
|
||||||
| UpdateLocationAction
|
| UpdateLocationAction
|
||||||
| LoadDataSourceTypesAction
|
| LoadDataSourceTypesAction
|
||||||
| SetDataSourceTypeSearchQueryAction;
|
| SetDataSourceTypeSearchQueryAction
|
||||||
|
| LoadDataSourceAction
|
||||||
|
| UpdateNavIndexAction
|
||||||
|
| LoadDataSourceMetaAction;
|
||||||
|
|
||||||
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
type ThunkResult<R> = ThunkAction<R, StoreState, undefined, Action>;
|
||||||
|
|
||||||
@ -80,6 +106,16 @@ export function loadDataSources(): ThunkResult<void> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function loadDataSource(id: number): ThunkResult<void> {
|
||||||
|
return async dispatch => {
|
||||||
|
const dataSource = await getBackendSrv().get(`/api/datasources/${id}`);
|
||||||
|
const pluginInfo = await getBackendSrv().get(`/api/plugins/${dataSource.type}/settings`);
|
||||||
|
dispatch(dataSourceLoaded(dataSource));
|
||||||
|
dispatch(dataSourceMetaLoaded(pluginInfo));
|
||||||
|
dispatch(updateNavIndex(buildNavModel(dataSource, pluginInfo)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function addDataSource(plugin: Plugin): ThunkResult<void> {
|
export function addDataSource(plugin: Plugin): ThunkResult<void> {
|
||||||
return async (dispatch, getStore) => {
|
return async (dispatch, getStore) => {
|
||||||
await dispatch(loadDataSources());
|
await dispatch(loadDataSources());
|
||||||
|
109
public/app/features/datasources/state/navModel.ts
Normal file
109
public/app/features/datasources/state/navModel.ts
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import { DataSource, NavModel, NavModelItem, PluginMeta } from 'app/types';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
|
||||||
|
export function buildNavModel(dataSource: DataSource, pluginMeta: PluginMeta): NavModelItem {
|
||||||
|
const navModel = {
|
||||||
|
img: pluginMeta.info.logos.large,
|
||||||
|
id: 'datasource-' + dataSource.id,
|
||||||
|
subTitle: `Type: ${pluginMeta.name}`,
|
||||||
|
url: '',
|
||||||
|
text: dataSource.name,
|
||||||
|
breadcrumbs: [{ title: 'Data Sources', url: 'datasources' }],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-sliders',
|
||||||
|
id: `datasource-settings-${dataSource.id}`,
|
||||||
|
text: 'Settings',
|
||||||
|
url: `datasources/edit/${dataSource.id}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginMeta.includes && hasDashboards(pluginMeta.includes)) {
|
||||||
|
navModel.children.push({
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-th-large',
|
||||||
|
id: `datasource-dashboards-${dataSource.id}`,
|
||||||
|
text: 'Dashboards',
|
||||||
|
url: `datasources/edit/${dataSource.id}/dashboards`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.buildInfo.isEnterprise) {
|
||||||
|
navModel.children.push({
|
||||||
|
active: false,
|
||||||
|
icon: 'fa fa-fw fa-lock',
|
||||||
|
id: `datasource-permissions-${dataSource.id}`,
|
||||||
|
text: 'Permissions',
|
||||||
|
url: `datasources/edit/${dataSource.id}/permissions`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return navModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDataSourceLoadingNav(pageName: string): NavModel {
|
||||||
|
const main = buildNavModel(
|
||||||
|
{
|
||||||
|
access: '',
|
||||||
|
basicAuth: false,
|
||||||
|
database: '',
|
||||||
|
id: 1,
|
||||||
|
isDefault: false,
|
||||||
|
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
|
||||||
|
name: 'Loading',
|
||||||
|
orgId: 1,
|
||||||
|
password: '',
|
||||||
|
readOnly: false,
|
||||||
|
type: 'Loading',
|
||||||
|
typeLogoUrl: 'public/img/icn-datasource.svg',
|
||||||
|
url: '',
|
||||||
|
user: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
name: '',
|
||||||
|
info: {
|
||||||
|
author: {
|
||||||
|
name: '',
|
||||||
|
url: '',
|
||||||
|
},
|
||||||
|
description: '',
|
||||||
|
links: [''],
|
||||||
|
logos: {
|
||||||
|
large: '',
|
||||||
|
small: '',
|
||||||
|
},
|
||||||
|
screenshots: '',
|
||||||
|
updated: '',
|
||||||
|
version: '',
|
||||||
|
},
|
||||||
|
includes: [{ type: '', name: '', path: '' }],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
let node: NavModelItem;
|
||||||
|
|
||||||
|
// find active page
|
||||||
|
for (const child of main.children) {
|
||||||
|
if (child.id.indexOf(pageName) > 0) {
|
||||||
|
child.active = true;
|
||||||
|
node = child;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
main: main,
|
||||||
|
node: node,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasDashboards(includes) {
|
||||||
|
return (
|
||||||
|
includes.filter(include => {
|
||||||
|
return include.type === 'dashboard';
|
||||||
|
}).length > 0
|
||||||
|
);
|
||||||
|
}
|
@ -4,11 +4,13 @@ import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelec
|
|||||||
|
|
||||||
const initialState: DataSourcesState = {
|
const initialState: DataSourcesState = {
|
||||||
dataSources: [] as DataSource[],
|
dataSources: [] as DataSource[],
|
||||||
|
dataSource: {} as DataSource,
|
||||||
layoutMode: LayoutModes.Grid,
|
layoutMode: LayoutModes.Grid,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
dataSourcesCount: 0,
|
dataSourcesCount: 0,
|
||||||
dataSourceTypes: [] as Plugin[],
|
dataSourceTypes: [] as Plugin[],
|
||||||
dataSourceTypeSearchQuery: '',
|
dataSourceTypeSearchQuery: '',
|
||||||
|
dataSourceMeta: {} as Plugin,
|
||||||
hasFetched: false,
|
hasFetched: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,6 +19,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
|
|||||||
case ActionTypes.LoadDataSources:
|
case ActionTypes.LoadDataSources:
|
||||||
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
return { ...state, hasFetched: true, dataSources: action.payload, dataSourcesCount: action.payload.length };
|
||||||
|
|
||||||
|
case ActionTypes.LoadDataSource:
|
||||||
|
return { ...state, dataSource: action.payload };
|
||||||
|
|
||||||
case ActionTypes.SetDataSourcesSearchQuery:
|
case ActionTypes.SetDataSourcesSearchQuery:
|
||||||
return { ...state, searchQuery: action.payload };
|
return { ...state, searchQuery: action.payload };
|
||||||
|
|
||||||
@ -28,6 +33,9 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo
|
|||||||
|
|
||||||
case ActionTypes.SetDataSourceTypeSearchQuery:
|
case ActionTypes.SetDataSourceTypeSearchQuery:
|
||||||
return { ...state, dataSourceTypeSearchQuery: action.payload };
|
return { ...state, dataSourceTypeSearchQuery: action.payload };
|
||||||
|
|
||||||
|
case ActionTypes.LoadDataSourceMeta:
|
||||||
|
return { ...state, dataSourceMeta: action.payload };
|
||||||
}
|
}
|
||||||
|
|
||||||
return state;
|
return state;
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { DataSource } from '../../../types';
|
||||||
|
|
||||||
export const getDataSources = state => {
|
export const getDataSources = state => {
|
||||||
const regex = new RegExp(state.searchQuery, 'i');
|
const regex = new RegExp(state.searchQuery, 'i');
|
||||||
|
|
||||||
@ -14,6 +16,13 @@ export const getDataSourceTypes = state => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDataSource = (state, dataSourceId): DataSource | null => {
|
||||||
|
if (state.dataSource.id === parseInt(dataSourceId, 10)) {
|
||||||
|
return state.dataSource;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
export const getDataSourcesSearchQuery = state => state.searchQuery;
|
export const getDataSourcesSearchQuery = state => state.searchQuery;
|
||||||
export const getDataSourcesLayoutMode = state => state.layoutMode;
|
export const getDataSourcesLayoutMode = state => state.layoutMode;
|
||||||
export const getDataSourcesCount = state => state.dataSourcesCount;
|
export const getDataSourcesCount = state => state.dataSourcesCount;
|
||||||
|
@ -644,7 +644,9 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{supportsTable && showingTable ? (
|
{supportsTable && showingTable ? (
|
||||||
<Table className="m-t-3" data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
|
<div className="panel-container">
|
||||||
|
<Table data={tableResult} loading={loading} onClickCell={this.onClickTableCell} />
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
|
{supportsLogs && showingLogs ? <Logs data={logsResult} loading={loading} /> : null}
|
||||||
</main>
|
</main>
|
||||||
|
@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => {
|
|||||||
|
|
||||||
it('returns label suggestions on label context but leaves out labels that already exist', () => {
|
it('returns label suggestions on label context but leaves out labels that already exist', () => {
|
||||||
const instance = shallow(
|
const instance = shallow(
|
||||||
<PromQueryField {...defaultProps} labelKeys={{ '{job="foo"}': ['bar', 'job'] }} />
|
<PromQueryField
|
||||||
|
{...defaultProps}
|
||||||
|
labelKeys={{ '{job1="foo",job2!="foo",job3=~"foo"}': ['bar', 'job1', 'job2', 'job3'] }}
|
||||||
|
/>
|
||||||
).instance() as PromQueryField;
|
).instance() as PromQueryField;
|
||||||
const value = Plain.deserialize('{job="foo",}');
|
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}');
|
||||||
const range = value.selection.merge({
|
const range = value.selection.merge({
|
||||||
anchorOffset: 11,
|
anchorOffset: 36,
|
||||||
});
|
});
|
||||||
const valueWithSelection = value.change().select(range).value;
|
const valueWithSelection = value.change().select(range).value;
|
||||||
const result = instance.getTypeahead({
|
const result = instance.getTypeahead({
|
||||||
@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => {
|
|||||||
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns label value suggestions inside a label value context after a negated matching operator', () => {
|
||||||
|
const instance = shallow(
|
||||||
|
<PromQueryField
|
||||||
|
{...defaultProps}
|
||||||
|
labelKeys={{ '{}': ['label'] }}
|
||||||
|
labelValues={{ '{}': { label: ['a', 'b', 'c'] } }}
|
||||||
|
/>
|
||||||
|
).instance() as PromQueryField;
|
||||||
|
const value = Plain.deserialize('{label!=}');
|
||||||
|
const range = value.selection.merge({ anchorOffset: 8 });
|
||||||
|
const valueWithSelection = value.change().select(range).value;
|
||||||
|
const result = instance.getTypeahead({
|
||||||
|
text: '!=',
|
||||||
|
prefix: '',
|
||||||
|
wrapperClasses: ['context-labels'],
|
||||||
|
labelKey: 'label',
|
||||||
|
value: valueWithSelection,
|
||||||
|
});
|
||||||
|
expect(result.context).toBe('context-label-values');
|
||||||
|
expect(result.suggestions).toEqual([
|
||||||
|
{
|
||||||
|
items: [{ label: 'a' }, { label: 'b' }, { label: 'c' }],
|
||||||
|
label: 'Label values for "label"',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('returns a refresher on label context and unavailable metric', () => {
|
it('returns a refresher on label context and unavailable metric', () => {
|
||||||
const instance = shallow(
|
const instance = shallow(
|
||||||
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
|
<PromQueryField {...defaultProps} labelKeys={{ '{__name__="foo"}': ['bar'] }} />
|
||||||
|
@ -111,7 +111,7 @@ export function willApplySuggestion(
|
|||||||
|
|
||||||
case 'context-label-values': {
|
case 'context-label-values': {
|
||||||
// Always add quotes and remove existing ones instead
|
// Always add quotes and remove existing ones instead
|
||||||
if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) {
|
if (!typeaheadText.match(/^(!?=~?"|")/)) {
|
||||||
suggestion = `"${suggestion}`;
|
suggestion = `"${suggestion}`;
|
||||||
}
|
}
|
||||||
if (getNextCharacter() !== '"') {
|
if (getNextCharacter() !== '"') {
|
||||||
@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
const containsMetric = selector.indexOf('__name__=') > -1;
|
const containsMetric = selector.indexOf('__name__=') > -1;
|
||||||
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
||||||
|
|
||||||
if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) {
|
if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) {
|
||||||
// Label values
|
// Label values
|
||||||
if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
|
if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) {
|
||||||
const labelValues = this.state.labelValues[selector][labelKey];
|
const labelValues = this.state.labelValues[selector][labelKey];
|
||||||
@ -571,10 +571,10 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
<button className="btn navbar-button navbar-button--tight">Log labels</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
) : (
|
) : (
|
||||||
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
<Cascader options={metricsOptions} onChange={this.onChangeMetrics}>
|
||||||
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
<button className="btn navbar-button navbar-button--tight">Metrics</button>
|
||||||
</Cascader>
|
</Cascader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="prom-query-field-wrapper">
|
<div className="prom-query-field-wrapper">
|
||||||
<div className="slate-query-field-wrapper">
|
<div className="slate-query-field-wrapper">
|
||||||
|
@ -228,7 +228,13 @@ class QueryField extends React.PureComponent<TypeaheadFieldProps, TypeaheadField
|
|||||||
const offset = range.startOffset;
|
const offset = range.startOffset;
|
||||||
const text = selection.anchorNode.textContent;
|
const text = selection.anchorNode.textContent;
|
||||||
let prefix = text.substr(0, offset);
|
let prefix = text.substr(0, offset);
|
||||||
if (cleanText) {
|
|
||||||
|
// Label values could have valid characters erased if `cleanText()` is
|
||||||
|
// blindly applied, which would undesirably interfere with suggestions
|
||||||
|
const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
|
||||||
|
if (labelValueMatch) {
|
||||||
|
prefix = labelValueMatch[1];
|
||||||
|
} else if (cleanText) {
|
||||||
prefix = cleanText(prefix);
|
prefix = cleanText(prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,84 +1,55 @@
|
|||||||
|
import _ from 'lodash';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
|
import ReactTable from 'react-table';
|
||||||
|
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
const EMPTY_TABLE = new TableModel();
|
const EMPTY_TABLE = new TableModel();
|
||||||
|
|
||||||
interface TableProps {
|
interface TableProps {
|
||||||
className?: string;
|
|
||||||
data: TableModel;
|
data: TableModel;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onClickCell?: (columnKey: string, rowValue: string) => void;
|
onClickCell?: (columnKey: string, rowValue: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SFCCellProps {
|
function prepareRows(rows, columnNames) {
|
||||||
columnIndex: number;
|
return rows.map(cells => _.zipObject(columnNames, cells));
|
||||||
onClickCell?: (columnKey: string, rowValue: string, columnIndex: number, rowIndex: number, table: TableModel) => void;
|
|
||||||
rowIndex: number;
|
|
||||||
table: TableModel;
|
|
||||||
value: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function Cell(props: SFCCellProps) {
|
export default class Table extends PureComponent<TableProps> {
|
||||||
const { columnIndex, rowIndex, table, value, onClickCell } = props;
|
getCellProps = (state, rowInfo, column) => {
|
||||||
const column = table.columns[columnIndex];
|
return {
|
||||||
if (column && column.filterable && onClickCell) {
|
onClick: () => {
|
||||||
const onClick = event => {
|
const columnKey = column.Header;
|
||||||
event.preventDefault();
|
const rowValue = rowInfo.row[columnKey];
|
||||||
onClickCell(column.text, value, columnIndex, rowIndex, table);
|
this.props.onClickCell(columnKey, rowValue);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
return (
|
};
|
||||||
<td>
|
|
||||||
<a className="link" onClick={onClick}>
|
|
||||||
{value}
|
|
||||||
</a>
|
|
||||||
</td>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return <td>{value}</td>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Table extends PureComponent<TableProps, {}> {
|
|
||||||
render() {
|
render() {
|
||||||
const { className = '', data, loading, onClickCell } = this.props;
|
const { data, loading } = this.props;
|
||||||
const tableModel = data || EMPTY_TABLE;
|
const tableModel = data || EMPTY_TABLE;
|
||||||
if (!loading && data && data.rows.length === 0) {
|
const columnNames = tableModel.columns.map(({ text }) => text);
|
||||||
return (
|
const columns = tableModel.columns.map(({ filterable, text }) => ({
|
||||||
<table className={`${className} filter-table`}>
|
Header: text,
|
||||||
<thead>
|
accessor: text,
|
||||||
<tr>
|
show: text !== 'Time',
|
||||||
<th>Table</th>
|
Cell: row => <span className={filterable ? 'link' : ''}>{row.value}</span>,
|
||||||
</tr>
|
}));
|
||||||
</thead>
|
const noDataText = data ? 'The queries returned no data for a table.' : '';
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<td className="muted">The queries returned no data for a table.</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<table className={`${className} filter-table`}>
|
<ReactTable
|
||||||
<thead>
|
columns={columns}
|
||||||
<tr>{tableModel.columns.map(col => <th key={col.text}>{col.text}</th>)}</tr>
|
data={tableModel.rows}
|
||||||
</thead>
|
getTdProps={this.getCellProps}
|
||||||
<tbody>
|
loading={loading}
|
||||||
{tableModel.rows.map((row, i) => (
|
minRows={0}
|
||||||
<tr key={i}>
|
noDataText={noDataText}
|
||||||
{row.map((value, j) => (
|
resolveData={data => prepareRows(data, columnNames)}
|
||||||
<Cell
|
showPagination={data}
|
||||||
key={j}
|
/>
|
||||||
columnIndex={j}
|
|
||||||
rowIndex={i}
|
|
||||||
value={String(value)}
|
|
||||||
table={data}
|
|
||||||
onClickCell={onClickCell}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user