diff --git a/.bra.toml b/.bra.toml index 15961e1e3fd..aa7a1680adc 100644 --- a/.bra.toml +++ b/.bra.toml @@ -4,6 +4,7 @@ init_cmds = [ ["./bin/grafana-server", "cfg:app_mode=development"] ] watch_all = true +follow_symlinks = true watch_dirs = [ "$WORKDIR/pkg", "$WORKDIR/public/views", diff --git a/.circleci/config.yml b/.circleci/config.yml index 861b4fa1fe3..87cd502ef18 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -170,6 +170,7 @@ jobs: - scripts/*.sh - scripts/publish - scripts/build/release_publisher/release_publisher + - scripts/build/publish.sh build: docker: @@ -237,8 +238,17 @@ jobs: steps: - checkout - run: - name: build, test and package grafana enterprise - command: './scripts/build/build_enterprise.sh' + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: checkout enterprise + command: './scripts/build/prepare_enterprise.sh' + - run: + name: test enterprise + command: 'go test ./pkg/extensions/...' + - run: + name: build and package enterprise + command: './scripts/build/build.sh -enterprise' - run: name: sign packages command: './scripts/build/sign_packages.sh' @@ -253,6 +263,53 @@ jobs: paths: - enterprise-dist/grafana-enterprise* + build-all-enterprise: + docker: + - image: grafana/build-container:1.2.0 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: checkout enterprise + command: './scripts/build/prepare_enterprise.sh' + - restore_cache: + key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }} + - run: + name: download phantomjs binaries + command: './scripts/build/download-phantomjs.sh' + - save_cache: + key: phantomjs-binaries-{{ checksum "scripts/build/download-phantomjs.sh" }} + paths: + - /tmp/phantomjs + - run: + name: test enterprise + command: 'go test ./pkg/extensions/...' + - run: + name: build and package grafana + command: './scripts/build/build-all.sh -enterprise' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: verify signed packages + command: | + mkdir -p ~/.rpmdb/pubkeys + curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key + ./scripts/build/verify_signed_packages.sh dist/*.rpm + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - run: + name: move enterprise packages into their own folder + command: 'mv dist enterprise-dist' + - persist_to_workspace: + root: . + paths: + - enterprise-dist/grafana-enterprise* + deploy-enterprise-master: docker: - image: circleci/python:2.7-stretch @@ -266,6 +323,19 @@ jobs: name: deploy to s3 command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master' +deploy-enterprise-release: + docker: + - image: circleci/python:2.7-stretch + steps: + - attach_workspace: + at: . + - run: + name: install awscli + command: 'sudo pip install awscli' + - run: + name: deploy to s3 + command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release' + deploy-master: docker: - image: circleci/python:2.7-stretch @@ -312,7 +382,7 @@ workflows: jobs: - build-all: filters: *filter-only-master - - build-enterprise: + - build-all-enterprise: filters: *filter-only-master - codespell: filters: *filter-only-master @@ -355,13 +425,15 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - - build-enterprise + - build-all-enterprise filters: *filter-only-master release: jobs: - build-all: filters: *filter-only-release + - build-all-enterprise: + filters: *filter-only-release - codespell: filters: *filter-only-release - gometalinter: @@ -384,6 +456,17 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-release + - deploy-enterprise-release: + requires: + - build-all + - build-all-enterprise + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-release - grafana-docker-release: requires: - build-all diff --git a/.gitignore b/.gitignore index d8c5bc49225..21083741e14 100644 --- a/.gitignore +++ b/.gitignore @@ -69,7 +69,6 @@ debug.test /vendor/**/*.yml /vendor/**/*_test.go /vendor/**/.editorconfig -/vendor/**/appengine* *.orig /devenv/bulk-dashboards/*.json diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fdcdc6a02..db5457147f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,20 +2,40 @@ ### New Features +* **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat) * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset) +* **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro) +* **MySQL**: Support connecting thru Unix socket for MySQL datasource [#12342](https://github.com/grafana/grafana/issues/12342), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) ### Minor +* **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) +* **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) -* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) ### Breaking changes * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited) -# 5.3.1 (unreleased) +# 5.3.2 (unreleased) + +* **InfluxDB/Graphite/Postgres**: Prevent cross site scripting (XSS) in query editor [#13667](https://github.com/grafana/grafana/issues/13667), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres**: Fix template variables error [#13692](https://github.com/grafana/grafana/issues/13692), thx [@svenklemm](https://github.com/svenklemm) +* **Cloudwatch**: Fix service panic because of race conditions [#13674](https://github.com/grafana/grafana/issues/13674), thx [@mtanda](https://github.com/mtanda) +* **Stackdriver/Cloudwatch**: Allow user to change unit in graph panel if cloudwatch/stackdriver datasource response doesn't include unit [#13718](https://github.com/grafana/grafana/issues/13718), thx [@mtanda](https://github.com/mtanda) +* **LDAP**: Fix super admins can also be admins of orgs [#13710](https://github.com/grafana/grafana/issues/13710), thx [@adrien-f](https://github.com/adrien-f) + +# 5.3.1 (2018-10-16) * **Render**: Fix PhantomJS render of graph panel when legend displayed as table to the right [#13616](https://github.com/grafana/grafana/issues/13616) +* **Stackdriver**: Filter option disappears after removing initial filter [#13607](https://github.com/grafana/grafana/issues/13607) +* **Elasticsearch**: Fix no limit size in terms aggregation for alerting queries [#13172](https://github.com/grafana/grafana/issues/13172), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) +* **InfluxDB**: Fix for annotation issue that caused text to be shown twice [#13553](https://github.com/grafana/grafana/issues/13553) +* **Variables**: Fix nesting variables leads to exception and missing refresh [#13628](https://github.com/grafana/grafana/issues/13628) +* **Variables**: Prometheus: Single letter labels are not supported [#13641](https://github.com/grafana/grafana/issues/13641), thx [@olshansky](https://github.com/olshansky) +* **Graph**: Fix graph time formatting for Last 24h ranges [#13650](https://github.com/grafana/grafana/issues/13650) +* **Playlist**: Fix cannot add dashboards with long names to playlist [#13464](https://github.com/grafana/grafana/issues/13464), thx [@neufeldtech](https://github.com/neufeldtech) +* **HTTP API**: Fix /api/org/users so that query and limit querystrings works # 5.3.0 (2018-10-10) @@ -68,7 +88,7 @@ * **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano) -* ****: **: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) +* **CloudWatch**: GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) * **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm) * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon) diff --git a/README.md b/README.md index 133d9e50d07..5882ea8a6a3 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.11 +- Go (Latest Stable) - NodeJS LTS ### Building the backend @@ -69,15 +69,27 @@ bra run Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). -### Building a docker image (on linux/amd64) +### Building a Docker image -This builds a docker image from your local sources: +There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker. + +Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev` + +#### Building on linux/amd64 (fast) 1. Build the frontend `go run build.go build-frontend` 2. Build the docker image `make build-docker-dev` The resulting image will be tagged as `grafana/grafana:dev` +#### Building anywhere (slower) + +Choose this option to build on platforms other than linux/amd64 and/or not have to setup the Grafana development environment. + +1. `make build-docker-full` or `docker build -t grafana/grafana:dev .` + +The resulting image will be tagged as `grafana/grafana:dev` + ### Dev config Create a custom.ini in the conf directory to override default configuration options. @@ -113,18 +125,6 @@ GRAFANA_TEST_DB=mysql go test ./pkg/... GRAFANA_TEST_DB=postgres go test ./pkg/... ``` -## Building custom docker image - -You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself. -```bash -git clone https://github.com/grafana/grafana -cd grafana -docker build -t grafana:dev . -docker run -d --name=grafana -p 3000:3000 grafana:dev -``` - -Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). - ## Contribute If you have any idea for an improvement or found a bug, do not hesitate to open an issue. @@ -138,5 +138,5 @@ plugin development. ## License -Grafana is distributed under Apache 2.0 License. +Grafana is distributed under [Apache 2.0 License](https://github.com/grafana/grafana/blob/master/LICENSE.md). diff --git a/UPGRADING_DEPENDENCIES.md b/UPGRADING_DEPENDENCIES.md new file mode 100644 index 00000000000..f3d2adbd71a --- /dev/null +++ b/UPGRADING_DEPENDENCIES.md @@ -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. \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 52f23162033..4bbd3668e19 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,7 +5,7 @@ os: Windows Server 2012 R2 clone_folder: c:\gopath\src\github.com\grafana\grafana environment: - nodejs_version: "6" + nodejs_version: "8" GOPATH: C:\gopath GOVERSION: 1.11 diff --git a/conf/defaults.ini b/conf/defaults.ini index eb8debc0094..750f06f2f6a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -554,3 +554,6 @@ container_name = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer server_url = callback_url = + +[panels] +enable_alpha = false diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 862bee3dfe5..8916b2bf6e3 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -156,9 +156,9 @@ Since not all datasources have the same configuration settings we only have the | tlsSkipVerify | boolean | *All* | Controls whether a client verifies the server's certificate chain and host name. | | graphiteVersion | string | Graphite | Graphite version | | timeInterval | string | Prometheus, Elasticsearch, InfluxDB, MySQL, PostgreSQL & MSSQL | Lowest interval/step value that should be used for this data source | -| esVersion | number | Elastic | Elasticsearch version as a number (2/5/56) | -| timeField | string | Elastic | Which field that should be used as timestamp | -| interval | string | Elastic | Index date time format | +| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56) | +| timeField | string | Elasticsearch | Which field that should be used as timestamp | +| interval | string | Elasticsearch | Index date time format | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | | assumeRoleArn | string | Cloudwatch | ARN of Assume Role | | defaultRegion | string | Cloudwatch | AWS region | @@ -166,6 +166,7 @@ Since not all datasources have the same configuration settings we only have the | tsdbVersion | string | OpenTSDB | Version | | tsdbResolution | string | OpenTSDB | Resolution | | sslmode | string | PostgreSQL | SSLmode. 'disable', 'require', 'verify-ca' or 'verify-full' | +| encrypt | string | MSSQL | Connection SSL encryption handling. 'disable', 'false' or 'true' | | postgresVersion | number | PostgreSQL | Postgres version as a number (903/904/905/906/1000) meaning v9.3, v9.4, ..., v10 | | timescaledb | boolean | PostgreSQL | Enable usage of TimescaleDB extension | | maxOpenConns | number | MySQL, PostgreSQL & MSSQL | Maximum number of open connections to the database (Grafana v5.4+) | diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index a5b7f4264e0..307af1ee15e 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -128,7 +128,7 @@ Example json body: In DingTalk PC Client: -1. Click "more" icon on left bottom of the panel. +1. Click "more" icon on upper right of the panel. 2. Click "Robot Manage" item in the pop menu, there will be a new panel call "Robot Manage". diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index 7adc6ebe4fb..be36d108475 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -46,7 +46,7 @@ Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGu ## IAM Policies Grafana needs permissions granted via IAM to be able to read CloudWatch metrics -and EC2 tags/instances. You can attach these permissions to IAM roles and +and EC2 tags/instances/regions. You can attach these permissions to IAM roles and utilize Grafana's built-in support for assuming roles. Here is a minimal policy example: @@ -65,11 +65,12 @@ Here is a minimal policy example: "Resource": "*" }, { - "Sid": "AllowReadingTagsFromEC2", + "Sid": "AllowReadingTagsInstancesRegionsFromEC2", "Effect": "Allow", "Action": [ "ec2:DescribeTags", - "ec2:DescribeInstances" + "ec2:DescribeInstances", + "ec2:DescribeRegions" ], "Resource": "*" } diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index 4a3478b161a..cd191f14273 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -32,6 +32,7 @@ Name | Description *Database* | Name of your MSSQL database. *User* | Database user's login/username *Password* | Database user's password +*Encrypt* | This option determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server, default `false` (Grafana v5.4+). *Max open* | The maximum number of open connections to the database, default `unlimited` (Grafana v5.4+). *Max idle* | The maximum number of connections in the idle connection pool, default `2` (Grafana v5.4+). *Max lifetime* | The maximum amount of time in seconds a connection may be reused, default `14400`/4 hours (Grafana v5.4+). @@ -72,8 +73,8 @@ Make sure the user does not get any unwanted privileges from the public role. ### Known Issues -MSSQL 2008 and 2008 R2 engine cannot handle login records when SSL encryption is not disabled. Due to this you may receive an `Login error: EOF` error when trying to create your datasource. -To fix MSSQL 2008 R2 issue, install MSSQL 2008 R2 Service Pack 2. To fix MSSQL 2008 issue, install Microsoft MSSQL 2008 Service Pack 3 and Cumulative update package 3 for MSSQL 2008 SP3. +If you're using an older version of Microsoft SQL Server like 2008 and 2008R2 you may need to disable encryption to be able to connect. +If possible, we recommend you to use the latest service pack available for optimal compatibility. ## Query Editor diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index cea86e96faf..103a58587b1 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -206,6 +206,7 @@ datasources: jsonData: tokenUri: https://oauth2.googleapis.com/token clientEmail: stackdriver@myproject.iam.gserviceaccount.com + defaultProject: my-project-name secureJsonData: privateKey: | -----BEGIN PRIVATE KEY----- diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index ba0d6199ba4..52353ede8c2 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -87,7 +87,7 @@ docker run \ ## Building a custom Grafana image with pre-installed plugins -In the [grafana-docker](https://github.com/grafana/grafana-docker/) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments. +In the [grafana-docker](https://github.com/grafana/grafana/tree/master/packaging/docker) there is a folder called `custom/` which includes a `Dockerfile` that can be used to build a custom Grafana image. It accepts `GRAFANA_VERSION` and `GF_INSTALL_PLUGINS` as build arguments. Example of how to build and run: ```bash @@ -103,6 +103,21 @@ docker run \ grafana:latest-with-plugins ``` +## Installing Plugins from other sources + +> Only available in Grafana v5.3.1+ + +It's possible to install plugins from custom url:s by specifying the url like this: `GF_INSTALL_PLUGINS=;` + +```bash +docker run \ + -d \ + -p 3000:3000 \ + --name=grafana \ + -e "GF_INSTALL_PLUGINS=http://plugin-domain.com/my-custom-plugin.zip;custom-plugin" \ + grafana/grafana +``` + ## Configuring AWS Credentials for CloudWatch Support ```bash diff --git a/docs/sources/plugins/developing/development.md b/docs/sources/plugins/developing/development.md index f2e70a50c6a..48410b06732 100644 --- a/docs/sources/plugins/developing/development.md +++ b/docs/sources/plugins/developing/development.md @@ -10,7 +10,7 @@ weight = 1 # Developer Guide -You can extend Grafana by writing your own plugins and then share then with other users in [our plugin repository](https://grafana.com/plugins). +You can extend Grafana by writing your own plugins and then share them with other users in [our plugin repository](https://grafana.com/plugins). ## Short version @@ -33,7 +33,7 @@ There are two blog posts about authoring a plugin that might also be of interest ## What languages? Since everything turns into javascript it's up to you to choose which language you want. That said it's probably a good idea to choose es6 or typescript since -we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo is you choose one of those languages. +we use es6 classes in Grafana. So it's easier to get inspiration from the Grafana repo if you choose one of those languages. ## Buildscript @@ -60,7 +60,6 @@ and [apps]({{< relref "apps.md" >}}) plugins in the documentation. The Grafana SDK is quite small so far and can be found here: - [SDK file in Grafana](https://github.com/grafana/grafana/blob/master/public/app/plugins/sdk.ts) -- [SDK Readme](https://github.com/grafana/grafana/blob/master/public/app/plugins/plugin_api.md) The SDK contains three different plugin classes: PanelCtrl, MetricsPanelCtrl and QueryCtrl. For plugins of the panel type, the module.js file should export one of these. There are some extra classes for [data sources]({{< relref "datasources.md" >}}). diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index ea75b9797e8..eed05f05fa6 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.11](https://golang.org/dl/) +- [Go (Latest Stable)](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) - [NodeJS LTS](https://nodejs.org/download/) - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. diff --git a/latest.json b/latest.json index 3b5a9e0c227..4355e9a64b7 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.3.0", - "testing": "5.3.0" + "stable": "5.3.1", + "testing": "5.3.1" } diff --git a/package.json b/package.json index bcef7093b2d..9b00853355a 100644 --- a/package.json +++ b/package.json @@ -80,7 +80,7 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-jest": "^23.1.4", + "ts-jest": "^23.10.4", "ts-loader": "^5.1.0", "tslib": "^1.9.3", "tslint": "^5.8.0", @@ -160,6 +160,7 @@ "react-redux": "^5.0.7", "react-select": "2.1.0", "react-sizeme": "^2.3.6", + "react-table": "^6.8.6", "react-transition-group": "^2.2.1", "redux": "^4.0.0", "redux-logger": "^3.0.6", diff --git a/pkg/api/api.go b/pkg/api/api.go index 3860bbae9c3..f1fe940e416 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -22,66 +22,66 @@ func (hs *HTTPServer) registerRoutes() { r := hs.RouteRegister // not logged in views - r.Get("/", reqSignedIn, Index) + r.Get("/", reqSignedIn, hs.Index) r.Get("/logout", Logout) r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost)) r.Get("/login/:name", quota("session"), OAuthLogin) - r.Get("/login", LoginView) - r.Get("/invite/:code", Index) + r.Get("/login", hs.LoginView) + r.Get("/invite/:code", hs.Index) // authed views - r.Get("/profile/", reqSignedIn, Index) - r.Get("/profile/password", reqSignedIn, Index) - r.Get("/profile/switch-org/:id", reqSignedIn, ChangeActiveOrgAndRedirectToHome) - r.Get("/org/", reqSignedIn, Index) - r.Get("/org/new", reqSignedIn, Index) - r.Get("/datasources/", reqSignedIn, Index) - r.Get("/datasources/new", reqSignedIn, Index) - r.Get("/datasources/edit/*", reqSignedIn, Index) - r.Get("/org/users", reqSignedIn, Index) - r.Get("/org/users/new", reqSignedIn, Index) - r.Get("/org/users/invite", reqSignedIn, Index) - r.Get("/org/teams", reqSignedIn, Index) - r.Get("/org/teams/*", reqSignedIn, Index) - r.Get("/org/apikeys/", reqSignedIn, Index) - r.Get("/dashboard/import/", reqSignedIn, Index) - r.Get("/configuration", reqGrafanaAdmin, Index) - r.Get("/admin", reqGrafanaAdmin, Index) - r.Get("/admin/settings", reqGrafanaAdmin, Index) - r.Get("/admin/users", reqGrafanaAdmin, Index) - r.Get("/admin/users/create", reqGrafanaAdmin, Index) - r.Get("/admin/users/edit/:id", reqGrafanaAdmin, Index) - r.Get("/admin/orgs", reqGrafanaAdmin, Index) - r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, Index) - r.Get("/admin/stats", reqGrafanaAdmin, Index) + r.Get("/profile/", reqSignedIn, hs.Index) + r.Get("/profile/password", reqSignedIn, hs.Index) + r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome) + r.Get("/org/", reqSignedIn, hs.Index) + r.Get("/org/new", reqSignedIn, hs.Index) + r.Get("/datasources/", reqSignedIn, hs.Index) + r.Get("/datasources/new", reqSignedIn, hs.Index) + r.Get("/datasources/edit/*", reqSignedIn, hs.Index) + r.Get("/org/users", reqSignedIn, hs.Index) + r.Get("/org/users/new", reqSignedIn, hs.Index) + r.Get("/org/users/invite", reqSignedIn, hs.Index) + r.Get("/org/teams", reqSignedIn, hs.Index) + r.Get("/org/teams/*", reqSignedIn, hs.Index) + r.Get("/org/apikeys/", reqSignedIn, hs.Index) + r.Get("/dashboard/import/", reqSignedIn, hs.Index) + r.Get("/configuration", reqGrafanaAdmin, hs.Index) + r.Get("/admin", reqGrafanaAdmin, hs.Index) + r.Get("/admin/settings", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users/create", reqGrafanaAdmin, hs.Index) + r.Get("/admin/users/edit/:id", reqGrafanaAdmin, hs.Index) + r.Get("/admin/orgs", reqGrafanaAdmin, hs.Index) + r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index) + r.Get("/admin/stats", reqGrafanaAdmin, hs.Index) - r.Get("/styleguide", reqSignedIn, Index) + r.Get("/styleguide", reqSignedIn, hs.Index) - r.Get("/plugins", reqSignedIn, Index) - r.Get("/plugins/:id/edit", reqSignedIn, Index) - r.Get("/plugins/:id/page/:page", reqSignedIn, Index) + r.Get("/plugins", reqSignedIn, hs.Index) + r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) + r.Get("/plugins/:id/page/:page", reqSignedIn, hs.Index) - r.Get("/d/:uid/:slug", reqSignedIn, Index) - r.Get("/d/:uid", reqSignedIn, Index) - r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, Index) - r.Get("/dashboard/script/*", reqSignedIn, Index) - r.Get("/dashboard-solo/snapshot/*", Index) - r.Get("/d-solo/:uid/:slug", reqSignedIn, Index) - r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, Index) - r.Get("/dashboard-solo/script/*", reqSignedIn, Index) - r.Get("/import/dashboard", reqSignedIn, Index) - r.Get("/dashboards/", reqSignedIn, Index) - r.Get("/dashboards/*", reqSignedIn, Index) + r.Get("/d/:uid/:slug", reqSignedIn, hs.Index) + r.Get("/d/:uid", reqSignedIn, hs.Index) + r.Get("/dashboard/db/:slug", reqSignedIn, redirectFromLegacyDashboardURL, hs.Index) + r.Get("/dashboard/script/*", reqSignedIn, hs.Index) + r.Get("/dashboard-solo/snapshot/*", hs.Index) + r.Get("/d-solo/:uid/:slug", reqSignedIn, hs.Index) + r.Get("/dashboard-solo/db/:slug", reqSignedIn, redirectFromLegacyDashboardSoloURL, hs.Index) + r.Get("/dashboard-solo/script/*", reqSignedIn, hs.Index) + r.Get("/import/dashboard", reqSignedIn, hs.Index) + r.Get("/dashboards/", reqSignedIn, hs.Index) + r.Get("/dashboards/*", reqSignedIn, hs.Index) - r.Get("/explore", reqEditorRole, Index) + r.Get("/explore", reqEditorRole, hs.Index) - r.Get("/playlists/", reqSignedIn, Index) - r.Get("/playlists/*", reqSignedIn, Index) - r.Get("/alerting/", reqSignedIn, Index) - r.Get("/alerting/*", reqSignedIn, Index) + r.Get("/playlists/", reqSignedIn, hs.Index) + r.Get("/playlists/*", reqSignedIn, hs.Index) + r.Get("/alerting/", reqSignedIn, hs.Index) + r.Get("/alerting/*", reqSignedIn, hs.Index) // sign up - r.Get("/signup", Index) + r.Get("/signup", hs.Index) r.Get("/api/user/signup/options", Wrap(GetSignUpOptions)) r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp)) r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2)) @@ -91,15 +91,15 @@ func (hs *HTTPServer) registerRoutes() { r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite)) // reset password - r.Get("/user/password/send-reset-email", Index) - r.Get("/user/password/reset", Index) + r.Get("/user/password/send-reset-email", hs.Index) + r.Get("/user/password/reset", hs.Index) r.Post("/api/user/password/send-reset-email", bind(dtos.SendResetPasswordEmailForm{}), Wrap(SendResetPasswordEmail)) r.Post("/api/user/password/reset", bind(dtos.ResetUserPasswordForm{}), Wrap(ResetPassword)) // dashboard snapshots - r.Get("/dashboard/snapshot/*", Index) - r.Get("/dashboard/snapshots/", reqSignedIn, Index) + r.Get("/dashboard/snapshot/*", hs.Index) + r.Get("/dashboard/snapshots/", reqSignedIn, hs.Index) // api for dashboard snapshots r.Post("/api/snapshots/", bind(m.CreateDashboardSnapshotCommand{}), CreateDashboardSnapshot) @@ -251,7 +251,7 @@ func (hs *HTTPServer) registerRoutes() { pluginRoute.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), Wrap(UpdatePluginSetting)) }, reqOrgAdmin) - apiRoute.Get("/frontend/settings/", GetFrontendSettings) + apiRoute.Get("/frontend/settings/", hs.GetFrontendSettings) apiRoute.Any("/datasources/proxy/:id/*", reqSignedIn, hs.ProxyDataSourceRequest) apiRoute.Any("/datasources/proxy/:id", reqSignedIn, hs.ProxyDataSourceRequest) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index d65598f6e5e..02248334b9c 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -6,6 +6,7 @@ import ( "os" "path" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/api/dtos" @@ -251,8 +252,8 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { return Error(403, err.Error(), err) } - if err == m.ErrDashboardContainsInvalidAlertData { - return Error(500, "Invalid alert data. Cannot save dashboard", err) + if validationErr, ok := err.(alerting.ValidationError); ok { + return Error(422, validationErr.Error(), nil) } if err != nil { diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 283a9b5f12c..2726623c242 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -9,6 +9,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" "github.com/grafana/grafana/pkg/setting" @@ -725,7 +726,7 @@ func TestDashboardApiEndpoint(t *testing.T) { {SaveError: m.ErrDashboardVersionMismatch, ExpectedStatusCode: 412}, {SaveError: m.ErrDashboardTitleEmpty, ExpectedStatusCode: 400}, {SaveError: m.ErrDashboardFolderCannotHaveParent, ExpectedStatusCode: 400}, - {SaveError: m.ErrDashboardContainsInvalidAlertData, ExpectedStatusCode: 500}, + {SaveError: alerting.ValidationError{Reason: "Mu"}, ExpectedStatusCode: 422}, {SaveError: m.ErrDashboardFailedToUpdateAlertData, ExpectedStatusCode: 500}, {SaveError: m.ErrDashboardFailedGenerateUniqueUid, ExpectedStatusCode: 500}, {SaveError: m.ErrDashboardTypeMismatch, ExpectedStatusCode: 400}, diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 6aedc051ab7..3bb2f236129 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -55,7 +55,6 @@ func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { dsId := c.ParamsInt64(":id") ds, err := hs.getDatasourceFromCache(dsId, c) - hs.log.Debug("We are in the ds proxy", "dsId", dsId) if err != nil { c.JsonApiErr(500, "Unable to load datasource meta data", err) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 46c5a41cdf8..e7614614076 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -17,24 +17,8 @@ func GetDataSources(c *m.ReqContext) Response { return Error(500, "Failed to query datasources", err) } - dsFilterQuery := m.DatasourcesPermissionFilterQuery{ - User: c.SignedInUser, - Datasources: query.Result, - } - - datasources := []*m.DataSource{} - if err := bus.Dispatch(&dsFilterQuery); err != nil { - if err != bus.ErrHandlerNotFound { - return Error(500, "Could not get datasources", err) - } - - datasources = query.Result - } else { - datasources = dsFilterQuery.Result - } - result := make(dtos.DataSourceList, 0) - for _, ds := range datasources { + for _, ds := range query.Result { dsItem := dtos.DataSourceListItemDTO{ OrgId: ds.OrgId, Id: ds.Id, diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 697d0a35a08..c037831f341 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -49,28 +49,30 @@ func formatShort(interval time.Duration) string { func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { return &AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - Frequency: formatShort(notification.Frequency), - SendReminder: notification.SendReminder, - Settings: notification.Settings, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: formatShort(notification.Frequency), + SendReminder: notification.SendReminder, + DisableResolveMessage: notification.DisableResolveMessage, + Settings: notification.Settings, } } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - SendReminder bool `json:"sendReminder"` - Frequency string `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Settings *simplejson.Json `json:"settings"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + SendReminder bool `json:"sendReminder"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -100,11 +102,12 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - SendReminder bool `json:"sendReminder"` - Frequency string `json:"frequency"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Frequency string `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 43fa0c858fc..1de65c7b2ce 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -11,7 +11,7 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { +func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { orgDataSources := make([]*m.DataSource, 0) if c.OrgId != 0 { @@ -133,6 +133,10 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) { panels := map[string]interface{}{} for _, panel := range enabledPlugins.Panels { + if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels { + continue + } + panels[panel.Id] = map[string]interface{}{ "module": panel.Module, "baseUrl": panel.BaseUrl, @@ -196,8 +200,8 @@ func getPanelSort(id string) int { return sort } -func GetFrontendSettings(c *m.ReqContext) { - settings, err := getFrontendSettingsMap(c) +func (hs *HTTPServer) GetFrontendSettings(c *m.ReqContext) { + settings, err := hs.getFrontendSettingsMap(c) if err != nil { c.JsonApiErr(400, "Failed to get frontend settings", err) return diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 432d6a18369..858b3c5a8c5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -28,6 +28,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" ) @@ -52,6 +53,7 @@ type HTTPServer struct { Bus bus.Bus `inject:""` RenderService rendering.Service `inject:""` Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` } func (hs *HTTPServer) Init() error { @@ -184,7 +186,7 @@ func (hs *HTTPServer) applyRoutes() { // then custom app proxy routes hs.initAppPluginRoutes(hs.macaron) // lastly not found route - hs.macaron.NotFound(NotFoundHandler) + hs.macaron.NotFound(hs.NotFoundHandler) } func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { diff --git a/pkg/api/index.go b/pkg/api/index.go index b78ee6e0a41..e61620f9586 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -17,8 +17,8 @@ const ( darkName = "dark" ) -func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { - settings, err := getFrontendSettingsMap(c) +func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { + settings, err := hs.getFrontendSettingsMap(c) if err != nil { return nil, err } @@ -316,19 +316,6 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { } if c.IsGrafanaAdmin { - children := []*dtos.NavLink{ - {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"}, - {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"}, - {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"}, - {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"}, - } - - if setting.IsEnterprise { - children = append(children, &dtos.NavLink{Text: "Licensing", Id: "licensing", Url: setting.AppSubUrl + "/admin/licensing", Icon: "fa fa-fw fa-unlock-alt"}) - } - - children = append(children, &dtos.NavLink{Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"}) - cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{ Text: "Server Admin", HideFromTabs: true, @@ -336,7 +323,13 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { Id: "admin", Icon: "gicon gicon-shield", Url: setting.AppSubUrl + "/admin/users", - Children: children, + Children: []*dtos.NavLink{ + {Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"}, + {Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"}, + {Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"}, + {Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"}, + {Text: "Style Guide", Id: "styleguide", Url: setting.AppSubUrl + "/styleguide", Icon: "fa fa-fw fa-eyedropper"}, + }, }) } @@ -357,11 +350,12 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) { }, }) + hs.HooksService.RunIndexDataHooks(&data) return &data, nil } -func Index(c *m.ReqContext) { - data, err := setIndexViewData(c) +func (hs *HTTPServer) Index(c *m.ReqContext) { + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return @@ -369,13 +363,13 @@ func Index(c *m.ReqContext) { c.HTML(200, "index", data) } -func NotFoundHandler(c *m.ReqContext) { +func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) { if c.IsApiRequest() { c.JsonApiErr(404, "Not found", nil) return } - data, err := setIndexViewData(c) + data, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return diff --git a/pkg/api/login.go b/pkg/api/login.go index 632d04e37f1..1083f89adfd 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -17,8 +17,8 @@ const ( ViewIndex = "index" ) -func LoginView(c *m.ReqContext) { - viewData, err := setIndexViewData(c) +func (hs *HTTPServer) LoginView(c *m.ReqContext) { + viewData, err := hs.setIndexViewData(c) if err != nil { c.Handle(500, "Failed to get settings", err) return diff --git a/pkg/api/user.go b/pkg/api/user.go index 4b916202e65..7116ad83f3f 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -177,17 +177,17 @@ func UserSetUsingOrg(c *m.ReqContext) Response { } // GET /profile/switch-org/:id -func ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { +func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *m.ReqContext) { orgID := c.ParamsInt64(":id") if !validateUsingOrg(c.UserId, orgID) { - NotFoundHandler(c) + hs.NotFoundHandler(c) } cmd := m.SetUsingOrgCommand{UserId: c.UserId, OrgId: orgID} if err := bus.Dispatch(&cmd); err != nil { - NotFoundHandler(c) + hs.NotFoundHandler(c) } c.Redirect(setting.AppSubUrl + "/") diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 84325bae808..06c07a2887c 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -100,7 +100,7 @@ func listenToSystemSignals(server *GrafanaServerImpl) { sighupChan := make(chan os.Signal, 1) signal.Notify(sighupChan, syscall.SIGHUP) - signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) for { select { diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 43f45f900d9..4c71ab3cd5f 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -185,7 +185,9 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo if ldapUser.isMemberOf(group.GroupDN) { extUser.OrgRoles[group.OrgId] = group.OrgRole - extUser.IsGrafanaAdmin = group.IsGrafanaAdmin + if extUser.IsGrafanaAdmin == nil || *extUser.IsGrafanaAdmin == false { + extUser.IsGrafanaAdmin = group.IsGrafanaAdmin + } } } diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 2128b469fa4..e0fd12937ed 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -23,38 +23,41 @@ var ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - SendReminder bool `json:"sendReminder"` - Frequency time.Duration `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - SendReminder bool `json:"sendReminder"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - SendReminder bool `json:"sendReminder"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + DisableResolveMessage bool `json:"disableResolveMessage"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 4b84d840113..e8aebb1d1f4 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -21,7 +21,6 @@ var ( ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ErrDashboardTitleEmpty = errors.New("Dashboard title cannot be empty") ErrDashboardFolderCannotHaveParent = errors.New("A Dashboard Folder cannot be added to another folder") - ErrDashboardContainsInvalidAlertData = errors.New("Invalid alert data. Cannot save dashboard") ErrDashboardFailedToUpdateAlertData = errors.New("Failed to save alert data") ErrDashboardsWithSameSlugExists = errors.New("Multiple dashboards with the same slug exists") ErrDashboardFailedGenerateUniqueUid = errors.New("Failed to generate unique dashboard id") diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 488fb2fe1fa..b71d17ec0d1 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -195,8 +195,8 @@ type GetDataSourceByNameQuery struct { type DsPermissionType int const ( - DsPermissionQuery DsPermissionType = 1 << iota - DsPermissionNoAccess + DsPermissionNoAccess DsPermissionType = iota + DsPermissionQuery ) func (p DsPermissionType) String() string { @@ -207,12 +207,6 @@ func (p DsPermissionType) String() string { return names[int(p)] } -type HasRequiredDataSourcePermissionQuery struct { - Id int64 - User *SignedInUser - RequiredPermission DsPermissionType -} - type GetDataSourcePermissionsForUserQuery struct { User *SignedInUser Result map[int64]DsPermissionType diff --git a/pkg/services/alerting/conditions/evaluator.go b/pkg/services/alerting/conditions/evaluator.go index 8d7ca57f010..eef593d39e2 100644 --- a/pkg/services/alerting/conditions/evaluator.go +++ b/pkg/services/alerting/conditions/evaluator.go @@ -2,6 +2,7 @@ package conditions import ( "encoding/json" + "fmt" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" @@ -31,12 +32,12 @@ type ThresholdEvaluator struct { func newThresholdEvaluator(typ string, model *simplejson.Json) (*ThresholdEvaluator, error) { params := model.Get("params").MustArray() if len(params) == 0 { - return nil, alerting.ValidationError{Reason: "Evaluator missing threshold parameter"} + return nil, fmt.Errorf("Evaluator missing threshold parameter") } firstParam, ok := params[0].(json.Number) if !ok { - return nil, alerting.ValidationError{Reason: "Evaluator has invalid parameter"} + return nil, fmt.Errorf("Evaluator has invalid parameter") } defaultEval := &ThresholdEvaluator{Type: typ} @@ -107,7 +108,7 @@ func (e *RangedEvaluator) Eval(reducedValue null.Float) bool { func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { typ := model.Get("type").MustString() if typ == "" { - return nil, alerting.ValidationError{Reason: "Evaluator missing type property"} + return nil, fmt.Errorf("Evaluator missing type property") } if inSlice(typ, defaultTypes) { @@ -122,7 +123,7 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { return &NoValueEvaluator{}, nil } - return nil, alerting.ValidationError{Reason: "Evaluator invalid evaluator type: " + typ} + return nil, fmt.Errorf("Evaluator invalid evaluator type: %s", typ) } func inSlice(a string, list []string) bool { diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 229092e217b..edfab2dedee 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -82,8 +82,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, if collapsed && collapsedJSON.MustBool() { // extract alerts from sub panels for collapsed panels - alertSlice, err := e.getAlertFromPanels(panel, - validateAlertFunc) + alertSlice, err := e.getAlertFromPanels(panel, validateAlertFunc) if err != nil { return nil, err } @@ -100,7 +99,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, panelID, err := panel.Get("id").Int64() if err != nil { - return nil, fmt.Errorf("panel id is required. err %v", err) + return nil, ValidationError{Reason: "A numeric panel id property is missing"} } // backward compatibility check, can be removed later @@ -146,7 +145,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, datasource, err := e.lookupDatasourceID(dsName) if err != nil { - return nil, err + e.log.Debug("Error looking up datasource", "error", err) + return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} } jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) @@ -167,8 +167,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, } if !validateAlertFunc(alert) { - e.log.Debug("Invalid Alert Data. Dashboard, Org or Panel ID is not correct", "alertName", alert.Name, "panelId", alert.PanelId) - return nil, m.ErrDashboardContainsInvalidAlertData + return nil, ValidationError{Reason: fmt.Sprintf("Panel id is not correct, alertName=%v, panelId=%v", alert.Name, alert.PanelId)} } alerts = append(alerts, alert) diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index c7212e48174..e2dc01a1181 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -258,7 +258,7 @@ func TestAlertRuleExtraction(t *testing.T) { Convey("Should fail on save", func() { _, err := extractor.GetAlerts() - So(err, ShouldEqual, m.ErrDashboardContainsInvalidAlertData) + So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") }) }) }) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 96294f0624f..040d0991861 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -27,6 +27,7 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool GetSendReminder() bool + GetDisableResolveMessage() bool GetFrequency() time.Duration } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index fbade2eccac..d141d6cd257 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -6,7 +6,6 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/alerting" ) @@ -15,13 +14,14 @@ const ( ) type NotifierBase struct { - Name string - Type string - Id int64 - IsDeault bool - UploadImage bool - SendReminder bool - Frequency time.Duration + Name string + Type string + Id int64 + IsDeault bool + UploadImage bool + SendReminder bool + DisableResolveMessage bool + Frequency time.Duration log log.Logger } @@ -34,14 +34,15 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { } return NotifierBase{ - Id: model.Id, - Name: model.Name, - IsDeault: model.IsDefault, - Type: model.Type, - UploadImage: uploadImage, - SendReminder: model.SendReminder, - Frequency: model.Frequency, - log: log.New("alerting.notifier." + model.Name), + Id: model.Id, + Name: model.Name, + IsDeault: model.IsDefault, + Type: model.Type, + UploadImage: uploadImage, + SendReminder: model.SendReminder, + DisableResolveMessage: model.DisableResolveMessage, + Frequency: model.Frequency, + log: log.New("alerting.notifier." + model.Name), } } @@ -83,6 +84,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC } } + // Do not notify when state is OK if DisableResolveMessage is set to true + if context.Rule.State == models.AlertStateOK && n.DisableResolveMessage { + return false + } + return true } @@ -106,6 +112,10 @@ func (n *NotifierBase) GetSendReminder() bool { return n.SendReminder } +func (n *NotifierBase) GetDisableResolveMessage() bool { + return n.DisableResolveMessage +} + func (n *NotifierBase) GetFrequency() time.Duration { return n.Frequency } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5e46d3ad72e..5062828cb4f 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -179,5 +179,10 @@ func TestBaseNotifier(t *testing.T) { base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) + + Convey("default value should be false for backwards compatibility", func() { + base := NewNotifierBase(model) + So(base.DisableResolveMessage, ShouldBeFalse) + }) }) } diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 5492de45d39..6c47c92972c 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -127,7 +127,13 @@ func (this *TelegramNotifier) buildMessageInlineImage(evalContext *alerting.Eval var err error imageFile, err = os.Open(evalContext.ImageOnDiskPath) - defer imageFile.Close() + defer func() { + err := imageFile.Close() + if err != nil { + log.Error2("Could not close Telegram inline image.", "err", err) + } + }() + if err != nil { return nil, err } diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index d13924c2a17..999611f15c4 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -36,13 +36,13 @@ type ValidationError struct { } func (e ValidationError) Error() string { - extraInfo := "" + extraInfo := e.Reason if e.Alertid != 0 { extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid) } if e.PanelId != 0 { - extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId) + extraInfo = fmt.Sprintf("%s PanelId: %v", extraInfo, e.PanelId) } if e.DashboardId != 0 { @@ -50,10 +50,10 @@ func (e ValidationError) Error() string { } if e.Err != nil { - return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo) + return fmt.Sprintf("Alert validation error: %s%s", e.Err.Error(), extraInfo) } - return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo) + return fmt.Sprintf("Alert validation error: %s", extraInfo) } var ( @@ -128,7 +128,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { } if len(model.Conditions) == 0 { - return nil, fmt.Errorf("Alert is missing conditions") + return nil, ValidationError{Reason: "Alert is missing conditions"} } return model, nil diff --git a/pkg/services/cleanup/cleanup.go b/pkg/services/cleanup/cleanup.go index 521601a358b..c15ae8ef36c 100644 --- a/pkg/services/cleanup/cleanup.go +++ b/pkg/services/cleanup/cleanup.go @@ -73,7 +73,7 @@ func (srv *CleanUpService) cleanUpTmpFiles() { } } - srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "keept", len(files)) + srv.log.Debug("Found old rendered image to delete", "deleted", len(toDelete), "kept", len(files)) } func (srv *CleanUpService) shouldCleanupTempFile(filemtime time.Time, now time.Time) bool { diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index 278421e6be7..8eb7f4a6e72 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -5,6 +5,7 @@ import ( "time" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/guardian" "github.com/grafana/grafana/pkg/util" @@ -25,7 +26,9 @@ type DashboardProvisioningService interface { // NewService factory for creating a new dashboard service var NewService = func() DashboardService { - return &dashboardServiceImpl{} + return &dashboardServiceImpl{ + log: log.New("dashboard-service"), + } } // NewProvisioningService factory for creating a new dashboard provisioning service @@ -45,6 +48,7 @@ type SaveDashboardDTO struct { type dashboardServiceImpl struct { orgId int64 user *models.SignedInUser + log log.Logger } func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) { @@ -89,7 +93,7 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, } if err := bus.Dispatch(&validateAlertsCmd); err != nil { - return nil, models.ErrDashboardContainsInvalidAlertData + return nil, err } } diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index f9d487f625c..b8300a5af8d 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -117,12 +117,12 @@ func TestDashboardService(t *testing.T) { }) bus.AddHandler("test", func(cmd *models.ValidateDashboardAlertsCommand) error { - return errors.New("error") + return errors.New("Alert validation error") }) dto.Dashboard = models.NewDashboard("Dash") _, err := service.SaveDashboard(dto) - So(err, ShouldEqual, models.ErrDashboardContainsInvalidAlertData) + So(err.Error(), ShouldEqual, "Alert validation error") }) }) diff --git a/pkg/services/hooks/hooks.go b/pkg/services/hooks/hooks.go new file mode 100644 index 00000000000..c51650cf6c9 --- /dev/null +++ b/pkg/services/hooks/hooks.go @@ -0,0 +1,30 @@ +package hooks + +import ( + "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/registry" +) + +type IndexDataHook func(indexData *dtos.IndexViewData) + +type HooksService struct { + indexDataHooks []IndexDataHook +} + +func init() { + registry.RegisterService(&HooksService{}) +} + +func (srv *HooksService) Init() error { + return nil +} + +func (srv *HooksService) AddIndexDataHook(hook IndexDataHook) { + srv.indexDataHooks = append(srv.indexDataHooks, hook) +} + +func (srv *HooksService) RunIndexDataHooks(indexData *dtos.IndexViewData) { + for _, hook := range srv.indexDataHooks { + hook(indexData) + } +} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index daaef945b96..afe6269510f 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -66,6 +66,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.updated, alert_notification.settings, alert_notification.is_default, + alert_notification.disable_resolve_message, alert_notification.send_reminder, alert_notification.frequency FROM alert_notification @@ -106,6 +107,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.updated, alert_notification.settings, alert_notification.is_default, + alert_notification.disable_resolve_message, alert_notification.send_reminder, alert_notification.frequency FROM alert_notification @@ -166,15 +168,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - SendReminder: cmd.SendReminder, - Frequency: frequency, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + SendReminder: cmd.SendReminder, + DisableResolveMessage: cmd.DisableResolveMessage, + Frequency: frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil { @@ -210,6 +213,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Type = cmd.Type current.IsDefault = cmd.IsDefault current.SendReminder = cmd.SendReminder + current.DisableResolveMessage = cmd.DisableResolveMessage if current.SendReminder { if cmd.Frequency == "" { @@ -224,7 +228,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Frequency = frequency } - sess.UseBool("is_default", "send_reminder") + sess.UseBool("is_default", "send_reminder", "disable_resolve_message") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index ed682bae5c6..629a6292eb5 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -219,6 +219,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result.OrgId, ShouldNotEqual, 0) So(cmd.Result.Type, ShouldEqual, "email") So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) + So(cmd.Result.DisableResolveMessage, ShouldBeFalse) Convey("Cannot save Alert Notification with the same name", func() { err = CreateAlertNotificationCommand(cmd) @@ -227,18 +228,20 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &models.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - SendReminder: true, - Frequency: "60s", - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: true, + DisableResolveMessage: true, + Frequency: "60s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) So(newCmd.Result.Name, ShouldEqual, "NewName") So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) + So(newCmd.Result.DisableResolveMessage, ShouldBeTrue) }) Convey("Can update alert notification to disable sending of reminders", func() { diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index cadcccf6c95..198a47b50ff 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -71,6 +71,9 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{ Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0", })) + mg.AddMigration("Add column disable_resolve_message", NewAddColumnMigration(alert_notification, &Column{ + Name: "disable_resolve_message", Type: DB_Bool, Nullable: false, Default: "0", + })) mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go index 5ad93dc7a3b..a0dd714fe6f 100644 --- a/pkg/services/sqlstore/user_auth_test.go +++ b/pkg/services/sqlstore/user_auth_test.go @@ -16,7 +16,6 @@ func TestUserAuth(t *testing.T) { Convey("Given 5 users", t, func() { var err error var cmd *m.CreateUserCommand - users := []m.User{} for i := 0; i < 5; i++ { cmd = &m.CreateUserCommand{ Email: fmt.Sprint("user", i, "@test.com"), @@ -25,7 +24,6 @@ func TestUserAuth(t *testing.T) { } err = CreateUser(context.Background(), cmd) So(err, ShouldBeNil) - users = append(users, cmd.Result) } Reset(func() { diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 58901e55c6b..16158ded002 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -213,6 +213,8 @@ type Cfg struct { TempDataLifetime time.Duration MetricsEndpointEnabled bool + + EnableAlphaPanels bool } type CommandLineArgs struct { @@ -694,6 +696,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { explore := iniFile.Section("explore") ExploreEnabled = explore.Key("enabled").MustBool(false) + panels := iniFile.Section("panels") + cfg.EnableAlphaPanels = panels.Key("enable_alpha").MustBool(false) + cfg.readSessionConfig() cfg.readSmtpSettings() cfg.readQuotaSettings() diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 61bbc04394a..437457df52a 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -86,9 +86,10 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc } func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) { - result := &tsdb.Response{ + results := &tsdb.Response{ Results: make(map[string]*tsdb.QueryResult), } + resultChan := make(chan *tsdb.QueryResult, len(queryContext.Queries)) eg, ectx := errgroup.WithContext(ctx) @@ -102,10 +103,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo RefId := queryContext.Queries[i].RefId query, err := parseQuery(queryContext.Queries[i].Model) if err != nil { - result.Results[RefId] = &tsdb.QueryResult{ + results.Results[RefId] = &tsdb.QueryResult{ Error: err, } - return result, nil + return results, nil } query.RefId = RefId @@ -118,10 +119,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo } if query.Id == "" && query.Expression != "" { - result.Results[query.RefId] = &tsdb.QueryResult{ + results.Results[query.RefId] = &tsdb.QueryResult{ Error: fmt.Errorf("Invalid query: id should be set if using expression"), } - return result, nil + return results, nil } eg.Go(func() error { @@ -130,12 +131,13 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo return err } if err != nil { - result.Results[query.RefId] = &tsdb.QueryResult{ + resultChan <- &tsdb.QueryResult{ + RefId: query.RefId, Error: err, } return nil } - result.Results[queryRes.RefId] = queryRes + resultChan <- queryRes return nil }) } @@ -149,10 +151,10 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo return err } for _, queryRes := range queryResponses { - result.Results[queryRes.RefId] = queryRes if err != nil { - result.Results[queryRes.RefId].Error = err + queryRes.Error = err } + resultChan <- queryRes } return nil }) @@ -162,8 +164,12 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo if err := eg.Wait(); err != nil { return nil, err } + close(resultChan) + for result := range resultChan { + results.Results[result.RefId] = result + } - return result, nil + return results, nil } func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) { diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index ee9d9583c4e..b74af76f09a 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -234,10 +234,37 @@ func parseMultiSelectValue(input string) []string { // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) { regions := []string{ - "ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1", "cn-northwest-1", - "eu-central-1", "eu-west-1", "eu-west-2", "eu-west-3", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2", "us-isob-east-1", "us-iso-east-1", + "ap-northeast-1", "ap-northeast-2", "ap-northeast-3", "ap-south-1", "ap-southeast-1", "ap-southeast-2", "ca-central-1", + "eu-central-1", "eu-north-1", "eu-west-1", "eu-west-2", "eu-west-3", "me-south-1", "sa-east-1", "us-east-1", "us-east-2", "us-west-1", "us-west-2", + "cn-north-1", "cn-northwest-1", "us-gov-east-1", "us-gov-west-1", "us-isob-east-1", "us-iso-east-1", } + err := e.ensureClientSession("us-east-1") + if err != nil { + return nil, err + } + r, err := e.ec2Svc.DescribeRegions(&ec2.DescribeRegionsInput{}) + if err != nil { + // ignore error for backward compatibility + plog.Error("Failed to get regions", "error", err) + } else { + for _, region := range r.Regions { + exists := false + + for _, existingRegion := range regions { + if existingRegion == *region.RegionName { + exists = true + break + } + } + + if !exists { + regions = append(regions, *region.RegionName) + } + } + } + sort.Strings(regions) + result := make([]suggestData, 0) for _, region := range regions { result = append(result, suggestData{Text: region, Value: region}) diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index 72e57d03fa0..469d6baa5de 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -52,13 +52,18 @@ func generateConnectionString(datasource *models.DataSource) string { } server, port := hostParts[0], hostParts[1] - return fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", + encrypt := datasource.JsonData.Get("encrypt").MustString("false") + connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", server, port, datasource.Database, datasource.User, password, ) + if encrypt != "false" { + connStr += fmt.Sprintf("encrypt=%s;", encrypt) + } + return connStr } type mssqlRowTransformer struct { diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index 8e48994c7ea..c3d4470603d 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -692,7 +692,7 @@ func TestMSSQL(t *testing.T) { }, } - resp, err := endpoint.Query(nil, nil, query) + resp, err := endpoint.Query(context.Background(), nil, query) So(err, ShouldBeNil) queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 645f6b49bbb..35b03e489a0 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -5,6 +5,7 @@ import ( "fmt" "reflect" "strconv" + "strings" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" @@ -20,10 +21,14 @@ func init() { func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { logger := log.New("tsdb.mysql") + protocol := "tcp" + if strings.HasPrefix(datasource.Url, "/") { + protocol = "unix" + } cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true", datasource.User, datasource.Password, - "tcp", + protocol, datasource.Url, datasource.Database, ) diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 7f12b8636bb..476e3ba6586 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -769,7 +769,7 @@ func TestMySQL(t *testing.T) { }, } - resp, err := endpoint.Query(nil, nil, query) + resp, err := endpoint.Query(context.Background(), nil, query) So(err, ShouldBeNil) queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index c0c04522dba..c381938aead 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -701,7 +701,7 @@ func TestPostgres(t *testing.T) { }, } - resp, err := endpoint.Query(nil, nil, query) + resp, err := endpoint.Query(context.Background(), nil, query) So(err, ShouldBeNil) queryResult := resp.Results["A"] So(queryResult.Error, ShouldBeNil) diff --git a/public/app/app.ts b/public/app/app.ts index 298bf5609cd..9647fbe5416 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -26,8 +26,12 @@ _.move = (array, fromIndex, toIndex) => { return array; }; -import { coreModule, registerAngularDirectives } from './core/core'; -import { setupAngularRoutes } from './routes/routes'; +import { coreModule, angularModules } from 'app/core/core_module'; +import { registerAngularDirectives } from 'app/core/core'; +import { setupAngularRoutes } from 'app/routes/routes'; + +import 'app/routes/GrafanaCtrl'; +import 'app/features/all'; // import symlinked extensions const extensionsIndex = (require as any).context('.', true, /extensions\/index.ts/); @@ -109,39 +113,26 @@ export class GrafanaApp { 'react', ]; - const moduleTypes = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; - - _.each(moduleTypes, type => { - const moduleName = 'grafana.' + type; - this.useModule(angular.module(moduleName, [])); - }); - // makes it possible to add dynamic stuff - this.useModule(coreModule); + _.each(angularModules, m => { + this.useModule(m); + }); // register react angular wrappers coreModule.config(setupAngularRoutes); registerAngularDirectives(); - const preBootRequires = [import('app/features/all')]; + // disable tool tip animation + $.fn.tooltip.defaults.animation = false; - Promise.all(preBootRequires) - .then(() => { - // disable tool tip animation - $.fn.tooltip.defaults.animation = false; - - // bootstrap the app - angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { - _.each(this.preBootModules, module => { - _.extend(module, this.registerFunctions); - }); - - this.preBootModules = null; - }); - }) - .catch(err => { - console.log('Application boot failed:', err); + // bootstrap the app + angular.bootstrap(document, this.ngModuleDependencies).invoke(() => { + _.each(this.preBootModules, module => { + _.extend(module, this.registerFunctions); }); + + this.preBootModules = null; + }); } } diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index da3fa2f8ab8..25a69b1f5e4 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -207,7 +207,7 @@ export class ManageDashboardsCtrl { const template = '' + - '`'; + ''; appEvents.emit('show-modal', { templateHtml: template, modalClass: 'modal--narrow', diff --git a/public/app/core/components/query_part/query_part_editor.ts b/public/app/core/components/query_part/query_part_editor.ts index 6181d020471..2cab966ed46 100644 --- a/public/app/core/components/query_part/query_part_editor.ts +++ b/public/app/core/components/query_part/query_part_editor.ts @@ -103,7 +103,7 @@ export function queryPartEditorDirective($compile, templateSrv) { $scope.$apply(() => { $scope.handleEvent({ $event: { name: 'get-param-options' } }).then(result => { const dynamicOptions = _.map(result, op => { - return op.value; + return _.escape(op.value); }); callback(dynamicOptions); }); @@ -117,6 +117,7 @@ export function queryPartEditorDirective($compile, templateSrv) { minLength: 0, items: 1000, updater: value => { + value = _.unescape(value); setTimeout(() => { inputBlur.call($input[0], paramIndex); }, 0); diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index bd355817f92..49931ecaac4 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -18,6 +18,7 @@ export function geminiScrollbar() { let scrollRoot = elem.parent(); const scroller = elem; + console.log('scroll'); if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; } diff --git a/public/app/core/components/sidemenu/SideMenu.tsx b/public/app/core/components/sidemenu/SideMenu.tsx index 0092c1e3842..fd3e0d95564 100644 --- a/public/app/core/components/sidemenu/SideMenu.tsx +++ b/public/app/core/components/sidemenu/SideMenu.tsx @@ -17,7 +17,7 @@ export class SideMenu extends PureComponent { render() { return [
- graphana_logo + Grafana
,
diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap index 6e3c474d245..ec2fa845c6d 100644 --- a/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap @@ -8,7 +8,7 @@ Array [ onClick={[Function]} > graphana_logo
, diff --git a/public/app/core/components/sql_part/sql_part_editor.ts b/public/app/core/components/sql_part/sql_part_editor.ts index 8097dddeb3b..1d29c577560 100644 --- a/public/app/core/components/sql_part/sql_part_editor.ts +++ b/public/app/core/components/sql_part/sql_part_editor.ts @@ -109,12 +109,12 @@ export function sqlPartEditorDirective($compile, templateSrv) { $scope.$apply(() => { $scope.handleEvent({ $event: { name: 'get-param-options', param: param } }).then(result => { const dynamicOptions = _.map(result, op => { - return op.value; + return _.escape(op.value); }); // add current value to dropdown if it's not in dynamicOptions if (_.indexOf(dynamicOptions, part.params[paramIndex]) === -1) { - dynamicOptions.unshift(part.params[paramIndex]); + dynamicOptions.unshift(_.escape(part.params[paramIndex])); } callback(dynamicOptions); @@ -129,6 +129,7 @@ export function sqlPartEditorDirective($compile, templateSrv) { minLength: 0, items: 1000, updater: value => { + value = _.unescape(value); if (value === part.params[paramIndex]) { clearTimeout(cancelBlur); $input.focus(); diff --git a/public/app/core/config.ts b/public/app/core/config.ts index bf5abe37d7f..1473f8a91f8 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,4 +1,5 @@ import _ from 'lodash'; +import { PanelPlugin } from 'app/types/plugins'; export interface BuildInfo { version: string; @@ -9,7 +10,7 @@ export interface BuildInfo { export class Settings { datasources: any; - panels: any; + panels: PanelPlugin[]; appSubUrl: string; windowTitlePrefix: string; buildInfo: BuildInfo; diff --git a/public/app/core/constants.ts b/public/app/core/constants.ts index 2642c5e400a..00981156614 100644 --- a/public/app/core/constants.ts +++ b/public/app/core/constants.ts @@ -8,3 +8,6 @@ export const DEFAULT_ROW_HEIGHT = 250; export const MIN_PANEL_HEIGHT = GRID_CELL_HEIGHT * 3; export const LS_PANEL_COPY_KEY = 'panel-copy'; + +export const DASHBOARD_TOOLBAR_HEIGHT = 55; +export const DASHBOARD_TOP_PADDING = 20; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 173d6b80b15..18a625d3307 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -19,7 +19,6 @@ import './components/colorpicker/spectrum_picker'; import './services/search_srv'; import './services/ng_react'; -import { grafanaAppDirective } from './components/grafana_app'; import { searchDirective } from './components/search/search'; import { infoPopover } from './components/info_popover'; import { navbarDirective } from './components/navbar/navbar'; @@ -60,7 +59,6 @@ export { registerAngularDirectives, arrayJoin, coreModule, - grafanaAppDirective, navbarDirective, searchDirective, liveSrv, diff --git a/public/app/core/core_module.ts b/public/app/core/core_module.ts index f6c30e6cf15..c8401975c18 100644 --- a/public/app/core/core_module.ts +++ b/public/app/core/core_module.ts @@ -1,2 +1,18 @@ import angular from 'angular'; -export default angular.module('grafana.core', ['ngRoute']); + +const coreModule = angular.module('grafana.core', ['ngRoute']); + +// legacy modules +const angularModules = [ + coreModule, + angular.module('grafana.controllers', []), + angular.module('grafana.directives', []), + angular.module('grafana.factories', []), + angular.module('grafana.services', []), + angular.module('grafana.filters', []), + angular.module('grafana.routes', []), +]; + +export { angularModules, coreModule }; + +export default coreModule; diff --git a/public/app/core/directives/dash_class.ts b/public/app/core/directives/dash_class.ts index 224bc2c772d..37124eb7d4b 100644 --- a/public/app/core/directives/dash_class.ts +++ b/public/app/core/directives/dash_class.ts @@ -2,16 +2,21 @@ import _ from 'lodash'; import coreModule from '../core_module'; /** @ngInject */ -export function dashClass() { +function dashClass($timeout) { return { link: ($scope, elem) => { - $scope.onAppEvent('panel-fullscreen-enter', () => { - elem.toggleClass('panel-in-fullscreen', true); + $scope.ctrl.dashboard.events.on('view-mode-changed', panel => { + console.log('view-mode-changed', panel.fullscreen); + if (panel.fullscreen) { + elem.addClass('panel-in-fullscreen'); + } else { + $timeout(() => { + elem.removeClass('panel-in-fullscreen'); + }); + } }); - $scope.onAppEvent('panel-fullscreen-exit', () => { - elem.toggleClass('panel-in-fullscreen', false); - }); + elem.toggleClass('panel-in-fullscreen', $scope.ctrl.dashboard.meta.fullscreen === true); $scope.$watch('ctrl.dashboardViewState.state.editview', newValue => { if (newValue) { diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index de904e95fc6..85576dcffee 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -3,7 +3,7 @@ import $ from 'jquery'; import coreModule from '../core_module'; /** @ngInject */ -export function metricSegment($compile, $sce) { +export function metricSegment($compile, $sce, templateSrv) { const inputTemplate = ' { const selected = _.find($scope.altSegments, { value: value }); if (selected) { segment.value = selected.value; - segment.html = selected.html || selected.value; + segment.html = selected.html || $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(selected.value)); segment.fake = false; segment.expandable = selected.expandable; @@ -56,7 +54,7 @@ export function metricSegment($compile, $sce) { } } else if (segment.custom !== 'false') { segment.value = value; - segment.html = $sce.trustAsHtml(value); + segment.html = $sce.trustAsHtml(templateSrv.highlightVariablesAsHtml(value)); segment.expandable = true; segment.fake = false; } @@ -95,7 +93,7 @@ export function metricSegment($compile, $sce) { // add custom values if (segment.custom !== 'false') { if (!segment.fake && _.indexOf(options, segment.value) === -1) { - options.unshift(segment.value); + options.unshift(_.escape(segment.value)); } } @@ -105,6 +103,7 @@ export function metricSegment($compile, $sce) { }; $scope.updater = value => { + value = _.unescape(value); if (value === segment.value) { clearTimeout(cancelBlur); $input.focus(); diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 2089cfe9f59..7c7dffd04b9 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -1,6 +1,7 @@ import { Action } from 'app/core/actions/location'; import { LocationState } from 'app/types'; import { renderUrl } from 'app/core/utils/url'; +import _ from 'lodash'; export const initialState: LocationState = { url: '', @@ -12,11 +13,17 @@ export const initialState: LocationState = { export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case 'UPDATE_LOCATION': { - const { path, query, routeParams } = action.payload; + const { path, routeParams } = action.payload; + let query = action.payload.query || state.query; + + if (action.payload.partial) { + query = _.defaults(query, state.query); + } + return { url: renderUrl(path || state.path, query), path: path || state.path, - query: query || state.query, + query: query, routeParams: routeParams || state.routeParams, }; } diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index ccd86856755..9b7ede59853 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; class DynamicDirectiveSrv { /** @ngInject */ - constructor(private $compile, private $rootScope) {} + constructor(private $compile) {} addDirective(element, name, scope) { const child = angular.element(document.createElement(name)); @@ -14,25 +14,19 @@ class DynamicDirectiveSrv { } link(scope, elem, attrs, options) { - options - .directive(scope) - .then(directiveInfo => { - if (!directiveInfo || !directiveInfo.fn) { - elem.empty(); - return; - } + const directiveInfo = options.directive(scope); + if (!directiveInfo || !directiveInfo.fn) { + elem.empty(); + return; + } - if (!directiveInfo.fn.registered) { - coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); - directiveInfo.fn.registered = true; - } + if (!directiveInfo.fn.registered) { + console.log('register panel tab'); + coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); + directiveInfo.fn.registered = true; + } - this.addDirective(elem, directiveInfo.name, scope); - }) - .catch(err => { - console.log('Plugin load:', err); - this.$rootScope.appEvent('alert-error', ['Plugin error', err.toString()]); - }); + this.addDirective(elem, directiveInfo.name, scope); } create(options) { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index d8dfc958dd4..f43dc96cd37 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -148,7 +148,7 @@ export class KeybindingSrv { this.bind('mod+o', () => { dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3; appEvents.emit('graph-hover-clear'); - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('mod+s', e => { @@ -257,7 +257,7 @@ export class KeybindingSrv { }); this.bind('d r', () => { - this.$rootScope.$broadcast('refresh'); + dashboard.startRefresh(); }); this.bind('d s', () => { diff --git a/public/app/core/specs/kbn.test.ts b/public/app/core/specs/kbn.test.ts index dfa665e3205..e621cdef632 100644 --- a/public/app/core/specs/kbn.test.ts +++ b/public/app/core/specs/kbn.test.ts @@ -399,6 +399,77 @@ describe('duration', () => { }); }); +describe('clock', () => { + it('null', () => { + const str = kbn.toClock(null, 0); + expect(str).toBe(''); + }); + it('size less than 1 second', () => { + const str = kbn.toClock(999, 0); + expect(str).toBe('999ms'); + }); + describe('size less than 1 minute', () => { + it('default', () => { + const str = kbn.toClock(59999); + expect(str).toBe('59s:999ms'); + }); + it('decimals equals 0', () => { + const str = kbn.toClock(59999, 0); + expect(str).toBe('59s'); + }); + }); + describe('size less than 1 hour', () => { + it('default', () => { + const str = kbn.toClock(3599999); + expect(str).toBe('59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = kbn.toClock(3599999, 0); + expect(str).toBe('59m'); + }); + it('decimals equals 1', () => { + const str = kbn.toClock(3599999, 1); + expect(str).toBe('59m:59s'); + }); + }); + describe('size greater than or equal 1 hour', () => { + it('default', () => { + const str = kbn.toClock(7199999); + expect(str).toBe('01h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = kbn.toClock(7199999, 0); + expect(str).toBe('01h'); + }); + it('decimals equals 1', () => { + const str = kbn.toClock(7199999, 1); + expect(str).toBe('01h:59m'); + }); + it('decimals equals 2', () => { + const str = kbn.toClock(7199999, 2); + expect(str).toBe('01h:59m:59s'); + }); + }); + describe('size greater than or equal 1 day', () => { + it('default', () => { + const str = kbn.toClock(89999999); + expect(str).toBe('24h:59m:59s:999ms'); + }); + it('decimals equals 0', () => { + const str = kbn.toClock(89999999, 0); + expect(str).toBe('24h'); + }); + it('decimals equals 1', () => { + const str = kbn.toClock(89999999, 1); + expect(str).toBe('24h:59m'); + }); + it('decimals equals 2', () => { + const str = kbn.toClock(89999999, 2); + expect(str).toBe('24h:59m:59s'); + }); + }); +}); + describe('volume', () => { it('1000m3', () => { const str = kbn.valueFormats['m3'](1000, 1, null); diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts index 064da13806b..4ee0dd7134b 100644 --- a/public/app/core/utils/dag.test.ts +++ b/public/app/core/utils/dag.test.ts @@ -104,5 +104,17 @@ describe('Directed acyclic graph', () => { const actual = nodeH.getOptimizedInputEdges(); expect(actual).toHaveLength(0); }); + + it('when linking non-existing input node with existing output node should throw error', () => { + expect(() => { + dag.link('non-existing', 'A'); + }).toThrowError("cannot link input node named non-existing since it doesn't exist in graph"); + }); + + it('when linking existing input node with non-existing output node should throw error', () => { + expect(() => { + dag.link('A', 'non-existing'); + }).toThrowError("cannot link output node named non-existing since it doesn't exist in graph"); + }); }); }); diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts index eb7ff1c3b1a..48c00a4c8c3 100644 --- a/public/app/core/utils/dag.ts +++ b/public/app/core/utils/dag.ts @@ -15,6 +15,14 @@ export class Edge { } link(inputNode: Node, outputNode: Node) { + if (!inputNode) { + throw Error('inputNode is required'); + } + + if (!outputNode) { + throw Error('outputNode is required'); + } + this.unlink(); this.inputNode = inputNode; this.outputNode = outputNode; @@ -152,7 +160,11 @@ export class Graph { for (let n = 0; n < inputArr.length; n++) { const i = inputArr[n]; if (typeof i === 'string') { - inputNodes.push(this.getNode(i)); + const n = this.getNode(i); + if (!n) { + throw Error(`cannot link input node named ${i} since it doesn't exist in graph`); + } + inputNodes.push(n); } else { inputNodes.push(i); } @@ -161,7 +173,11 @@ export class Graph { for (let n = 0; n < outputArr.length; n++) { const i = outputArr[n]; if (typeof i === 'string') { - outputNodes.push(this.getNode(i)); + const n = this.getNode(i); + if (!n) { + throw Error(`cannot link output node named ${i} since it doesn't exist in graph`); + } + outputNodes.push(n); } else { outputNodes.push(i); } diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index bd69f2e89d9..398ad9bb3e7 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -808,6 +808,51 @@ kbn.toDuration = (size, decimals, timeScale) => { return strings.join(', '); }; +kbn.toClock = (size, decimals) => { + if (size === null) { + return ''; + } + + // < 1 second + if (size < 1000) { + return moment.utc(size).format('SSS\\m\\s'); + } + + // < 1 minute + if (size < 60000) { + let format = 'ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'ss\\s'; + } + return moment.utc(size).format(format); + } + + // < 1 hour + if (size < 3600000) { + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + if (decimals === 0) { + format = 'mm\\m'; + } else if (decimals === 1) { + format = 'mm\\m:ss\\s'; + } + return moment.utc(size).format(format); + } + + let format = 'mm\\m:ss\\s:SSS\\m\\s'; + + const hours = `${('0' + Math.floor(moment.duration(size, 'milliseconds').asHours())).slice(-2)}h`; + + if (decimals === 0) { + format = ''; + } else if (decimals === 1) { + format = 'mm\\m'; + } else if (decimals === 2) { + format = 'mm\\m:ss\\s'; + } + + return format ? `${hours}:${moment.utc(size).format(format)}` : hours; +}; + kbn.valueFormats.dtdurationms = (size, decimals) => { return kbn.toDuration(size, decimals, 'millisecond'); }; @@ -824,6 +869,14 @@ kbn.valueFormats.timeticks = (size, decimals, scaledDecimals) => { return kbn.valueFormats.s(size / 100, decimals, scaledDecimals); }; +kbn.valueFormats.clockms = (size, decimals) => { + return kbn.toClock(size, decimals); +}; + +kbn.valueFormats.clocks = (size, decimals) => { + return kbn.toClock(size * 1000, decimals); +}; + kbn.valueFormats.dateTimeAsIso = (epoch, isUtc) => { const time = isUtc ? moment.utc(epoch) : moment(epoch); @@ -901,6 +954,8 @@ kbn.getUnitFormats = () => { { text: 'duration (s)', value: 'dtdurations' }, { text: 'duration (hh:mm:ss)', value: 'dthms' }, { text: 'Timeticks (s/100)', value: 'timeticks' }, + { text: 'clock (ms)', value: 'clockms' }, + { text: 'clock (s)', value: 'clocks' }, ], }, { diff --git a/public/app/features/alerting/NotificationsEditCtrl.ts b/public/app/features/alerting/NotificationsEditCtrl.ts index 315a9a619a1..2607121bb0e 100644 --- a/public/app/features/alerting/NotificationsEditCtrl.ts +++ b/public/app/features/alerting/NotificationsEditCtrl.ts @@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { defaults: any = { type: 'email', sendReminder: false, + disableResolveMessage: false, frequency: '15m', settings: { httpMethod: 'POST', diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 7b198736b83..b2cd2f21e4d 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -21,21 +21,28 @@ + + diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index f9820fe566d..19850da52d9 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -8,6 +8,7 @@ import { makeRegions, dedupAnnotations } from './events_processing'; export class AnnotationsSrv { globalAnnotationsPromise: any; alertStatesPromise: any; + datasourcePromises: any; /** @ngInject */ constructor(private $rootScope, private $q, private datasourceSrv, private backendSrv, private timeSrv) { @@ -18,6 +19,7 @@ export class AnnotationsSrv { clearCache() { this.globalAnnotationsPromise = null; this.alertStatesPromise = null; + this.datasourcePromises = null; } getAnnotations(options) { @@ -90,6 +92,7 @@ export class AnnotationsSrv { const range = this.timeSrv.timeRange(); const promises = []; + const dsPromises = []; for (const annotation of dashboard.annotations.list) { if (!annotation.enable) { @@ -99,10 +102,10 @@ export class AnnotationsSrv { if (annotation.snapshotData) { return this.translateQueryResult(annotation, annotation.snapshotData); } - + const datasourcePromise = this.datasourceSrv.get(annotation.datasource); + dsPromises.push(datasourcePromise); promises.push( - this.datasourceSrv - .get(annotation.datasource) + datasourcePromise .then(datasource => { // issue query against data source return datasource.annotationQuery({ @@ -122,7 +125,7 @@ export class AnnotationsSrv { }) ); } - + this.datasourcePromises = this.$q.all(dsPromises); this.globalAnnotationsPromise = this.$q.all(promises); return this.globalAnnotationsPromise; } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index f75743513f1..5ec4e5e3929 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -22,7 +22,6 @@ import './export_data/export_data_modal'; import './ad_hoc_filters'; import './repeat_option/repeat_option'; import './dashgrid/DashboardGridDirective'; -import './dashgrid/PanelLoader'; import './dashgrid/RowOptions'; import './folder_picker/folder_picker'; import './move_to_folder_modal/move_to_folder'; diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index d61eb08d5b9..c34b9ddaff2 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -1,11 +1,10 @@ import config from 'app/core/config'; import coreModule from 'app/core/core_module'; -import { PanelContainer } from './dashgrid/PanelContainer'; import { DashboardModel } from './dashboard_model'; import { PanelModel } from './panel_model'; -export class DashboardCtrl implements PanelContainer { +export class DashboardCtrl { dashboard: DashboardModel; dashboardViewState: any; loadedFallbackDashboard: boolean; @@ -22,8 +21,7 @@ export class DashboardCtrl implements PanelContainer { private dashboardSrv, private unsavedChangesSrv, private dashboardViewStateSrv, - public playlistSrv, - private panelLoader + public playlistSrv ) { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet @@ -119,14 +117,6 @@ export class DashboardCtrl implements PanelContainer { return this.dashboard; } - getPanelLoader() { - return this.panelLoader; - } - - timezoneChanged() { - this.$rootScope.$broadcast('refresh'); - } - getPanelContainer() { return this; } @@ -168,10 +158,17 @@ export class DashboardCtrl implements PanelContainer { this.dashboard.removePanel(panel); } + onDestroy() { + if (this.dashboard) { + this.dashboard.destroy(); + } + } + init(dashboard) { this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); + this.$scope.$on('$destroy', this.onDestroy.bind(this)); this.setupDashboard(dashboard); } } diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 818765124bf..65a234a2b94 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -200,6 +200,43 @@ export class DashboardModel { this.events.emit('view-mode-changed', panel); } + timeRangeUpdated() { + this.events.emit('time-range-updated'); + } + + startRefresh() { + this.events.emit('refresh'); + + for (const panel of this.panels) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + } + + render() { + this.events.emit('render'); + + for (const panel of this.panels) { + panel.render(); + } + } + + panelInitialized(panel: PanelModel) { + if (!this.otherPanelInFullscreen(panel)) { + panel.refresh(); + } + } + + otherPanelInFullscreen(panel: PanelModel) { + return this.meta.fullscreen && !panel.fullscreen; + } + + changePanelType(panel: PanelModel, pluginId: string) { + panel.changeType(pluginId); + this.events.emit('panel-type-changed', panel); + } + private ensureListExist(data) { if (!data) { data = {}; diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index a26a0401d56..68cee112f42 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import classNames from 'classnames'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; @@ -11,7 +11,7 @@ import Highlighter from 'react-highlight-words'; export interface AddPanelPanelProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export interface AddPanelPanelState { @@ -93,8 +93,7 @@ export class AddPanelPanel extends React.Component { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); + const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; const newPanel: any = { @@ -123,9 +122,7 @@ export class AddPanelPanel extends React.Component PanelContainer; + dashboard: DashboardModel; } export class DashboardGrid extends React.Component { gridToPanelMap: any; - panelContainer: PanelContainer; - dashboard: DashboardModel; panelMap: { [id: string]: PanelModel }; constructor(props) { super(props); - this.panelContainer = this.props.getPanelContainer(); this.onLayoutChange = this.onLayoutChange.bind(this); this.onResize = this.onResize.bind(this); this.onResizeStop = this.onResizeStop.bind(this); @@ -81,20 +77,21 @@ export class DashboardGrid extends React.Component { this.state = { animated: false }; // subscribe to dashboard events - this.dashboard = this.panelContainer.getDashboard(); - this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); - this.dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('view-mode-changed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); - this.dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + const dashboard = this.props.dashboard; + dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-removed', this.triggerForceUpdate.bind(this)); + dashboard.on('repeats-processed', this.triggerForceUpdate.bind(this)); + dashboard.on('view-mode-changed', this.onViewModeChanged.bind(this)); + dashboard.on('row-collapsed', this.triggerForceUpdate.bind(this)); + dashboard.on('row-expanded', this.triggerForceUpdate.bind(this)); + dashboard.on('panel-type-changed', this.triggerForceUpdate.bind(this)); } buildLayout() { const layout = []; this.panelMap = {}; - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { const stringId = panel.id.toString(); this.panelMap[stringId] = panel; @@ -129,7 +126,7 @@ export class DashboardGrid extends React.Component { this.panelMap[newPos.i].updateGridPos(newPos); } - this.dashboard.sortPanelsByGridPos(); + this.props.dashboard.sortPanelsByGridPos(); } triggerForceUpdate() { @@ -137,11 +134,15 @@ export class DashboardGrid extends React.Component { } onWidthChange() { - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { panel.resizeDone(); } } + onViewModeChanged(payload) { + this.setState({ animated: !payload.fullscreen }); + } + updateGridPos(item, layout) { this.panelMap[item.i].updateGridPos(item); @@ -165,21 +166,18 @@ export class DashboardGrid extends React.Component { componentDidMount() { setTimeout(() => { - this.setState(() => { - return { animated: true }; - }); + this.setState({ animated: true }); }); } renderPanels() { const panelElements = []; - for (const panel of this.dashboard.panels) { + for (const panel of this.props.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( - /** panel-id is set for html bookmarks */ -
- +
+
); } @@ -192,8 +190,8 @@ export class DashboardGrid extends React.Component { PanelContainer; + dashboard: DashboardModel; } -export class DashboardPanel extends React.Component { +export interface State { + pluginExports: PluginExports; +} + +export class DashboardPanel extends React.Component { element: any; - attachedPanel: AttachedPanel; + angularPanel: AngularComponent; + pluginInfo: any; + specialPanels = {}; constructor(props) { super(props); - this.state = {}; + + this.state = { + pluginExports: null, + }; + + this.specialPanels['row'] = this.renderRow.bind(this); + this.specialPanels['add-panel'] = this.renderAddPanel.bind(this); } - componentDidMount() { - if (!this.element) { + isSpecial() { + return this.specialPanels[this.props.panel.type]; + } + + renderRow() { + return ; + } + + renderAddPanel() { + return ; + } + + onPluginTypeChanged = (plugin: PanelPlugin) => { + this.props.panel.changeType(plugin.id); + this.loadPlugin(); + }; + + onAngularPluginTypeChanged = () => { + this.loadPlugin(); + }; + + loadPlugin() { + if (this.isSpecial()) { return; } - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - const loader = panelContainer.getPanelLoader(); - this.attachedPanel = loader.load(this.element, this.props.panel, dashboard); + // handle plugin loading & changing of plugin type + if (!this.pluginInfo || this.pluginInfo.id !== this.props.panel.type) { + this.pluginInfo = config.panels[this.props.panel.type]; + + if (this.pluginInfo.exports) { + this.cleanUpAngularPanel(); + this.setState({ pluginExports: this.pluginInfo.exports }); + } else { + importPluginModule(this.pluginInfo.module).then(pluginExports => { + this.cleanUpAngularPanel(); + // cache plugin exports (saves a promise async cycle next time) + this.pluginInfo.exports = pluginExports; + // update panel state + this.setState({ pluginExports: pluginExports }); + }); + } + } + } + + componentDidMount() { + this.loadPlugin(); + } + + componentDidUpdate() { + this.loadPlugin(); + + // handle angular plugin loading + if (!this.element || this.angularPanel) { + return; + } + + const loader = getAngularLoader(); + const template = ''; + const scopeProps = { panel: this.props.panel, dashboard: this.props.dashboard }; + this.angularPanel = loader.load(this.element, scopeProps, template); + } + + cleanUpAngularPanel() { + if (this.angularPanel) { + this.angularPanel.destroy(); + this.angularPanel = null; + } } componentWillUnmount() { - if (this.attachedPanel) { - this.attachedPanel.destroy(); - } + this.cleanUpAngularPanel(); + } + + renderReactPanel() { + const { pluginExports } = this.state; + const containerClass = this.props.panel.isEditing ? 'panel-editor-container' : 'panel-height-helper'; + const panelWrapperClass = this.props.panel.isEditing ? 'panel-editor-container__panel' : 'panel-height-helper'; + + // this might look strange with these classes that change when edit, but + // I want to try to keep markup (parents) for panel the same in edit mode to avoide unmount / new mount of panel + return ( +
+
+ +
+ {this.props.panel.isEditing && ( +
+ +
+ )} +
+ ); } render() { - // special handling for rows - if (this.props.panel.type === 'row') { - return ; + if (this.isSpecial()) { + return this.specialPanels[this.props.panel.type](); } - if (this.props.panel.type === 'add-panel') { - return ; + if (!this.state.pluginExports) { + return null; } - return ( -
this.element = element} className="panel-height-helper" /> - ); + if (this.state.pluginExports.PanelComponent) { + return this.renderReactPanel(); + } + + // legacy angular rendering + return
(this.element = element)} className="panel-height-helper" />; } } - diff --git a/public/app/features/dashboard/dashgrid/DashboardRow.tsx b/public/app/features/dashboard/dashgrid/DashboardRow.tsx index 378cf4c2c7c..5b8ced9b2b1 100644 --- a/public/app/features/dashboard/dashgrid/DashboardRow.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardRow.tsx @@ -1,19 +1,16 @@ import React from 'react'; import classNames from 'classnames'; import { PanelModel } from '../panel_model'; -import { PanelContainer } from './PanelContainer'; +import { DashboardModel } from '../dashboard_model'; import templateSrv from 'app/features/templating/template_srv'; import appEvents from 'app/core/app_events'; export interface DashboardRowProps { panel: PanelModel; - getPanelContainer: () => PanelContainer; + dashboard: DashboardModel; } export class DashboardRow extends React.Component { - dashboard: any; - panelContainer: any; - constructor(props) { super(props); @@ -21,9 +18,6 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - this.panelContainer = this.props.getPanelContainer(); - this.dashboard = this.panelContainer.getDashboard(); - this.toggle = this.toggle.bind(this); this.openSettings = this.openSettings.bind(this); this.delete = this.delete.bind(this); @@ -31,7 +25,7 @@ export class DashboardRow extends React.Component { } toggle() { - this.dashboard.toggleRow(this.props.panel); + this.props.dashboard.toggleRow(this.props.panel); this.setState(prevState => { return { collapsed: !prevState.collapsed }; @@ -39,7 +33,7 @@ export class DashboardRow extends React.Component { } update() { - this.dashboard.processRepeats(); + this.props.dashboard.processRepeats(); this.forceUpdate(); } @@ -61,14 +55,10 @@ export class DashboardRow extends React.Component { altActionText: 'Delete row only', icon: 'fa-trash', onConfirm: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, true); + this.props.dashboard.removeRow(this.props.panel, true); }, onAltAction: () => { - const panelContainer = this.props.getPanelContainer(); - const dashboard = panelContainer.getDashboard(); - dashboard.removeRow(this.props.panel, false); + this.props.dashboard.removeRow(this.props.panel, false); }, }); } @@ -87,7 +77,7 @@ export class DashboardRow extends React.Component { const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars); const count = this.props.panel.panels ? this.props.panel.panels.length : 0; const panels = count === 1 ? 'panel' : 'panels'; - const canEdit = this.dashboard.meta.canEdit === true; + const canEdit = this.props.dashboard.meta.canEdit === true; return (
diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx new file mode 100644 index 00000000000..d0122363668 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -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 { + 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 ( +
+

Loading

+
+ ); + } + + return ( + <> + {this.loadingSpinner} + {this.props.children({ + timeSeries, + loading, + })} + + ); + } + + private get loadingSpinner(): JSX.Element { + const { loading } = this.state; + + if (loading === LoadingState.Loading) { + return ( +
+ +
+ ); + } + + return null; + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx new file mode 100644 index 00000000000..82b366d8126 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -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; +} + +export interface State { + refreshCounter: number; + timeRange?: TimeRange; +} + +export class PanelChrome extends PureComponent { + 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 ( +
+ +
+ + {({ loading, timeSeries }) => { + return ; + }} + +
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelContainer.ts b/public/app/features/dashboard/dashgrid/PanelContainer.ts deleted file mode 100644 index 87f3235a176..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelContainer.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { DashboardModel } from '../dashboard_model'; -import { PanelLoader } from './PanelLoader'; - -export interface PanelContainer { - getPanelLoader(): PanelLoader; - getDashboard(): DashboardModel; -} diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx new file mode 100644 index 00000000000..26ac8b7d2c1 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -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 { + 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 ; + } + + renderPanelOptions() { + const { pluginExports } = this.props; + + if (pluginExports.PanelOptions) { + const PanelOptions = pluginExports.PanelOptions; + return ; + } else { + return

Visualization has no options

; + } + } + + renderVizTab() { + return ( +
+
+ +
+
+
Options
+ {this.renderPanelOptions()} +
+
+ ); + } + + onChangeTab = (tab: PanelEditorTab) => { + store.dispatch( + updateLocation({ + query: { tab: tab.id }, + partial: true, + }) + ); + }; + + render() { + const { location } = store.getState(); + const activeTab = location.query.tab || 'queries'; + + return ( +
+
+
    + {this.tabs.map(tab => { + return ; + })} +
+ + +
+ +
+ {activeTab === 'queries' && this.renderQueriesTab()} + {activeTab === 'visualization' && this.renderVizTab()} +
+
+ ); + } +} + +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 ( +
  • + onClick(tab)}> + {tab.text} + +
  • + ); +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx new file mode 100644 index 00000000000..12d5cd37253 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader.tsx @@ -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 { + 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 ( +
    + + + + + + {isLoading && ( + + + + )} + +
    + + + {this.props.panel.title} + + + + + + 4m + + +
    +
    + ); + } +} diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx new file mode 100644 index 00000000000..f13f212826a --- /dev/null +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -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 { + element: any; + component: AngularComponent; + + constructor(props) { + super(props); + } + + componentDidMount() { + if (!this.element) { + return; + } + + const { panel, dashboard } = this.props; + + const loader = getAngularLoader(); + const template = ''; + 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
    (this.element = element)} className="panel-height-helper" />; + } +} diff --git a/public/app/features/dashboard/dashgrid/VizTypePicker.tsx b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx new file mode 100644 index 00000000000..9402133df34 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/VizTypePicker.tsx @@ -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 { + 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 ( +
    this.props.onTypeChanged(plugin)} title={plugin.name}> + +
    {plugin.name}
    +
    + ); + }; + + render() { + return ( +
    +
    +
    + +
    +
    +
    + +
    {this.state.pluginList.map(this.renderVizPlugin)}
    +
    +
    +
    + ); + } +} diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index c4095e7948b..7312d6db784 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -42,6 +42,8 @@ export class DashNavCtrl { } else if (search.fullscreen) { delete search.fullscreen; delete search.edit; + delete search.tab; + delete search.panelId; } this.$location.search(search); } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index 9a1e7fb9200..ebf8a6bb224 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -13,6 +13,13 @@ const notPersistedProperties: { [str: string]: boolean } = { events: true, fullscreen: true, isEditing: true, + hasRefreshed: true, +}; + +const defaults: any = { + gridPos: { x: 0, y: 0, h: 3, w: 6 }, + datasource: null, + targets: [{}], }; export class PanelModel { @@ -31,10 +38,14 @@ export class PanelModel { collapsed?: boolean; panels?: any; soloMode?: boolean; + targets: any[]; + datasource: string; + thresholds?: any; // non persisted fullscreen: boolean; isEditing: boolean; + hasRefreshed: boolean; events: Emitter; constructor(model) { @@ -45,9 +56,8 @@ export class PanelModel { this[property] = model[property]; } - if (!this.gridPos) { - this.gridPos = { x: 0, y: 0, h: 3, w: 6 }; - } + // defaults + _.defaultsDeep(this, _.cloneDeep(defaults)); } getSaveModel() { @@ -57,6 +67,10 @@ export class PanelModel { continue; } + if (_.isEqual(this[property], defaults[property])) { + continue; + } + model[property] = _.cloneDeep(this[property]); } @@ -82,7 +96,6 @@ export class PanelModel { this.gridPos.h = newPos.h; if (sizeChanged) { - console.log('PanelModel sizeChanged event and render events fired'); this.events.emit('panel-size-changed'); } } @@ -91,6 +104,34 @@ export class PanelModel { this.events.emit('panel-size-changed'); } + refresh() { + this.hasRefreshed = true; + this.events.emit('refresh'); + } + + render() { + if (!this.hasRefreshed) { + this.refresh(); + } else { + this.events.emit('render'); + } + } + + panelInitialized() { + this.events.emit('panel-initialized'); + } + + initEditMode() { + this.events.emit('panel-init-edit-mode'); + } + + changeType(pluginId: string) { + this.type = pluginId; + + delete this.thresholds; + delete this.alert; + } + destroy() { this.events.removeAllListeners(); } diff --git a/public/app/features/dashboard/settings/settings.ts b/public/app/features/dashboard/settings/settings.ts index 048a51efead..b6a70ee4b98 100755 --- a/public/app/features/dashboard/settings/settings.ts +++ b/public/app/features/dashboard/settings/settings.ts @@ -32,7 +32,7 @@ export class SettingsCtrl { this.$scope.$on('$destroy', () => { this.dashboard.updateSubmenuVisibility(); - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); setTimeout(() => { this.$rootScope.appEvent('dash-scroll', { restore: true }); }); diff --git a/public/app/features/dashboard/share_snapshot_ctrl.ts b/public/app/features/dashboard/share_snapshot_ctrl.ts index ec487801948..ac09d63054d 100644 --- a/public/app/features/dashboard/share_snapshot_ctrl.ts +++ b/public/app/features/dashboard/share_snapshot_ctrl.ts @@ -46,8 +46,7 @@ export class ShareSnapshotCtrl { $scope.loading = true; $scope.snapshot.external = external; - - $rootScope.$broadcast('refresh'); + $scope.dashboard.startRefresh(); $timeout(() => { $scope.saveSnapshot(external); diff --git a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx index 872d9296d12..c5f66fed32a 100644 --- a/public/app/features/dashboard/specs/AddPanelPanel.test.tsx +++ b/public/app/features/dashboard/specs/AddPanelPanel.test.tsx @@ -14,7 +14,7 @@ jest.mock('app/core/store', () => ({ })); describe('AddPanelPanel', () => { - let wrapper, dashboardMock, getPanelContainer, panel; + let wrapper, dashboardMock, panel; beforeEach(() => { config.panels = [ @@ -23,6 +23,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Singlestat', sort: 2, + module: '', + baseUrl: '', + meta: {}, info: { logos: { small: '', @@ -34,6 +37,9 @@ describe('AddPanelPanel', () => { hideFromList: true, name: 'Hidden', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -45,6 +51,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Graph', sort: 1, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -56,6 +65,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Zabbix', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -67,6 +79,9 @@ describe('AddPanelPanel', () => { hideFromList: false, name: 'Piechart', sort: 100, + meta: {}, + module: '', + baseUrl: '', info: { logos: { small: '', @@ -77,13 +92,8 @@ describe('AddPanelPanel', () => { dashboardMock = { toggleRow: jest.fn() }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn(), - }); - panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); }); it('should fetch all panels sorted with core plugins first', () => { diff --git a/public/app/features/dashboard/specs/DashboardRow.test.tsx b/public/app/features/dashboard/specs/DashboardRow.test.tsx index 3d89c22f962..77c6cb39d9d 100644 --- a/public/app/features/dashboard/specs/DashboardRow.test.tsx +++ b/public/app/features/dashboard/specs/DashboardRow.test.tsx @@ -4,7 +4,7 @@ import { DashboardRow } from '../dashgrid/DashboardRow'; import { PanelModel } from '../panel_model'; describe('DashboardRow', () => { - let wrapper, panel, getPanelContainer, dashboardMock; + let wrapper, panel, dashboardMock; beforeEach(() => { dashboardMock = { @@ -14,13 +14,8 @@ describe('DashboardRow', () => { }, }; - getPanelContainer = jest.fn().mockReturnValue({ - getDashboard: jest.fn().mockReturnValue(dashboardMock), - getPanelLoader: jest.fn(), - }); - panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); }); 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', () => { dashboardMock.meta.canEdit = false; - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find('.dashboard-row__drag')).toHaveLength(0); }); it('should have zero actions when cannot edit', () => { dashboardMock.meta.canEdit = false; panel = new PanelModel({ collapsed: false }); - wrapper = shallow(); + wrapper = shallow(); expect(wrapper.find('.dashboard-row__actions .pointer')).toHaveLength(0); }); }); diff --git a/public/app/features/dashboard/specs/exporter.test.ts b/public/app/features/dashboard/specs/exporter.test.ts index c7a232f925b..f21e151f3dd 100644 --- a/public/app/features/dashboard/specs/exporter.test.ts +++ b/public/app/features/dashboard/specs/exporter.test.ts @@ -240,5 +240,5 @@ stubs['-- Grafana --'] = { }; function getStub(arg) { - return Promise.resolve(stubs[arg]); + return Promise.resolve(stubs[arg || 'gfdb']); } diff --git a/public/app/features/dashboard/specs/viewstate_srv.test.ts b/public/app/features/dashboard/specs/viewstate_srv.test.ts index 905ffb8b355..f9963afbf85 100644 --- a/public/app/features/dashboard/specs/viewstate_srv.test.ts +++ b/public/app/features/dashboard/specs/viewstate_srv.test.ts @@ -2,6 +2,7 @@ import 'app/features/dashboard/view_state_srv'; import config from 'app/core/config'; import { DashboardViewState } from '../view_state_srv'; +import { DashboardModel } from '../dashboard_model'; describe('when updating view state', () => { const location = { @@ -10,14 +11,13 @@ describe('when updating view state', () => { }; const $scope = { + appEvent: jest.fn(), onAppEvent: jest.fn(() => {}), - dashboard: { - meta: {}, - panels: [], - }, + dashboard: new DashboardModel({ + panels: [{ id: 1 }], + }), }; - const $rootScope = {}; let viewState; beforeEach(() => { @@ -33,7 +33,7 @@ describe('when updating view state', () => { location.search = jest.fn(() => { 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', () => { @@ -55,7 +55,7 @@ describe('when updating view state', () => { describe('to fullscreen false', () => { beforeEach(() => { - viewState = new DashboardViewState($scope, location, {}, $rootScope); + viewState = new DashboardViewState($scope, location, {}); }); it('should remove params from query string', () => { viewState.update({ fullscreen: true, panelId: 1, edit: true }); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 9e923f6bcb7..749edef58fb 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -1,7 +1,8 @@ import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from 'app/core/services/backend_srv'; - +import appEvents from 'app/core/app_events'; +import { loadPluginDashboards } from '../../plugins/state/actions'; import { DashboardAcl, DashboardAclDTO, @@ -113,3 +114,18 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar await dispatch(getDashboardPermissions(dashboardId)); }; } + +export function importDashboard(data, dashboardTitle: string): ThunkResult { + 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 { + return async dispatch => { + await getBackendSrv().delete(`/api/dashboards/${uri}`); + dispatch(loadPluginDashboards()); + }; +} diff --git a/public/app/features/dashboard/submenu/submenu.ts b/public/app/features/dashboard/submenu/submenu.ts index e1288b2b2ed..184d29facee 100644 --- a/public/app/features/dashboard/submenu/submenu.ts +++ b/public/app/features/dashboard/submenu/submenu.ts @@ -7,13 +7,13 @@ export class SubmenuCtrl { dashboard: any; /** @ngInject */ - constructor(private $rootScope, private variableSrv, private $location) { + constructor(private variableSrv, private $location) { this.annotations = this.dashboard.templating.list; this.variables = this.variableSrv.variables; } annotationStateChanged() { - this.$rootScope.$broadcast('refresh'); + this.dashboard.startRefresh(); } variableUpdated(variable) { diff --git a/public/app/features/dashboard/time_srv.ts b/public/app/features/dashboard/time_srv.ts index a96bc89daa7..03b4a408125 100644 --- a/public/app/features/dashboard/time_srv.ts +++ b/public/app/features/dashboard/time_srv.ts @@ -1,8 +1,14 @@ +// Libraries import moment from 'moment'; import _ from 'lodash'; -import coreModule from 'app/core/core_module'; + +// Utils import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; import * as dateMath from 'app/core/utils/datemath'; +// Types + +import { TimeRange } from 'app/types'; export class TimeSrv { time: any; @@ -24,7 +30,6 @@ export class TimeSrv { document.addEventListener('visibilitychange', () => { if (this.autoRefreshBlocked && document.visibilityState === 'visible') { this.autoRefreshBlocked = false; - this.refreshDashboard(); } }); @@ -142,7 +147,7 @@ export class TimeSrv { } refreshDashboard() { - this.$rootScope.$broadcast('refresh'); + this.dashboard.timeRangeUpdated(); } private startNextRefreshTimer(afterMs) { @@ -201,7 +206,7 @@ export class TimeSrv { return range; } - timeRange() { + timeRange(): TimeRange { // make copies if they are moment (do not want to return out internal moment, because they are mutable!) const raw = { 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 center = range.to.valueOf() - timespan / 2; - let to = center + timespan * factor / 2; - let from = center - timespan * factor / 2; - - if (to > Date.now() && range.to <= Date.now()) { - const offset = to - Date.now(); - from = from - offset; - to = Date.now(); - } + const to = center + timespan * factor / 2; + const from = center - timespan * factor / 2; 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); diff --git a/public/app/features/dashboard/timepicker/settings.html b/public/app/features/dashboard/timepicker/settings.html index 3cb8ca061fb..fd5170013c2 100644 --- a/public/app/features/dashboard/timepicker/settings.html +++ b/public/app/features/dashboard/timepicker/settings.html @@ -5,7 +5,7 @@
    - +
    diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index c133203cefc..c89e49b54b3 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -31,9 +31,10 @@ export class TimePickerCtrl { $rootScope.onAppEvent('shift-time-forward', () => 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); + this.dashboard.on('refresh', this.onRefresh.bind(this), $scope); + // init options this.panel = this.dashboard.timepicker; _.defaults(this.panel, TimePickerCtrl.defaults); diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index d9ad6827567..8805050831e 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import _ from 'lodash'; import config from 'app/core/config'; +import appEvents from 'app/core/app_events'; import { DashboardModel } from './dashboard_model'; // represents the transient view state @@ -10,12 +11,11 @@ export class DashboardViewState { panelScopes: any; $scope: any; dashboard: DashboardModel; - editStateChanged: any; fullscreenPanel: any; oldTimeRange: any; /** @ngInject */ - constructor($scope, private $location, private $timeout, private $rootScope) { + constructor($scope, private $location, private $timeout) { const self = this; self.state = {}; self.panelScopes = []; @@ -33,10 +33,6 @@ export class DashboardViewState { 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 // don't want url changes like adding orgId to add browser history $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); this.dashboard.meta.fullscreen = this.state.fullscreen; @@ -124,110 +117,59 @@ export class DashboardViewState { } syncState() { - if (this.panelScopes.length === 0) { - return; - } - if (this.dashboard.meta.fullscreen) { - const panelScope = this.getPanelScope(this.state.panelId); - if (!panelScope) { + const panel = this.dashboard.getPanelById(this.state.panelId); + + if (!panel) { return; } - if (this.fullscreenPanel) { - // if already fullscreen - if (this.fullscreenPanel === panelScope && this.editStateChanged === false) { - return; - } else { - this.leaveFullscreen(false); - } - } - - if (!panelScope.ctrl.editModeInitiated) { - panelScope.ctrl.initEditMode(); - } - - if (!panelScope.ctrl.fullscreen) { - this.enterFullscreen(panelScope); + if (!panel.fullscreen) { + this.enterFullscreen(panel); + } else { + // already in fullscreen view just update the view mode + this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { - this.leaveFullscreen(true); + this.leaveFullscreen(); } } - getPanelScope(id) { - return _.find(this.panelScopes, panelScope => { - return panelScope.ctrl.panel.id === id; - }); - } + leaveFullscreen() { + const panel = this.fullscreenPanel; - leaveFullscreen(render) { - const self = this; - const ctrl = self.fullscreenPanel.ctrl; + this.dashboard.setViewMode(panel, false, false); - ctrl.editMode = false; - 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; - } + delete this.fullscreenPanel; this.$timeout(() => { - if (self.oldTimeRange !== ctrl.range) { - self.$rootScope.$broadcast('refresh'); + appEvents.emit('dash-scroll', { restore: true }); + + if (this.oldTimeRange !== this.dashboard.time) { + this.dashboard.startRefresh(); } else { - self.$rootScope.$broadcast('render'); + this.dashboard.render(); } - delete self.fullscreenPanel; }); - return true; } - enterFullscreen(panelScope) { - const ctrl = panelScope.ctrl; + enterFullscreen(panel) { + const isEditing = this.state.edit && this.dashboard.meta.canEdit; - ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit; - ctrl.fullscreen = true; - - this.oldTimeRange = ctrl.range; - this.fullscreenPanel = panelScope; + this.oldTimeRange = this.dashboard.time; + this.fullscreenPanel = panel; // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - this.dashboard.setViewMode(ctrl.panel, true, ctrl.editMode); - 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(); - }); + this.dashboard.setViewMode(panel, true, isEditing); } } /** @ngInject */ -export function dashboardViewStateSrv($location, $timeout, $rootScope) { +export function dashboardViewStateSrv($location, $timeout) { return { create: $scope => { - return new DashboardViewState($scope, $location, $timeout, $rootScope); + return new DashboardViewState($scope, $location, $timeout); }, }; } diff --git a/public/app/features/datasources/AddDataSourcePermissions.test.tsx b/public/app/features/datasources/AddDataSourcePermissions.test.tsx deleted file mode 100644 index facd71b51b7..00000000000 --- a/public/app/features/datasources/AddDataSourcePermissions.test.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { AddDataSourcePermissions, Props } from './AddDataSourcePermissions'; -import { AclTarget } from '../../types/acl'; - -const setup = () => { - const props: Props = { - onAddPermission: jest.fn(), - onCancel: jest.fn(), - }; - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render user picker', () => { - const wrapper = setup(); - - wrapper.instance().setState({ type: AclTarget.User }); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/datasources/DashboardsTable.test.tsx b/public/app/features/datasources/DashboardsTable.test.tsx new file mode 100644 index 00000000000..10a14f464e4 --- /dev/null +++ b/public/app/features/datasources/DashboardsTable.test.tsx @@ -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(); +}; + +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(); + }); +}); diff --git a/public/app/features/datasources/DashboardsTable.tsx b/public/app/features/datasources/DashboardsTable.tsx new file mode 100644 index 00000000000..b732782c23b --- /dev/null +++ b/public/app/features/datasources/DashboardsTable.tsx @@ -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 = ({ dashboards, onImport, onRemove }) => { + function buttonText(dashboard: PluginDashboard) { + return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import'; + } + + return ( + + + {dashboards.map((dashboard, index) => { + return ( + + + + + + ); + })} + +
    + + + {dashboard.imported ? ( + {dashboard.title} + ) : ( + {dashboard.title} + )} + + {!dashboard.imported ? ( + + ) : ( + + )} + {dashboard.imported && ( + + )} +
    + ); +}; + +export default DashboardsTable; diff --git a/public/app/features/datasources/DataSourceDashboards.test.tsx b/public/app/features/datasources/DataSourceDashboards.test.tsx new file mode 100644 index 00000000000..7409572b9cd --- /dev/null +++ b/public/app/features/datasources/DataSourceDashboards.test.tsx @@ -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(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/datasources/DataSourceDashboards.tsx b/public/app/features/datasources/DataSourceDashboards.tsx new file mode 100644 index 00000000000..51f9c9ca945 --- /dev/null +++ b/public/app/features/datasources/DataSourceDashboards.tsx @@ -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 { + 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 ( +
    + +
    + this.onImport(dashboard, overwrite)} + onRemove={dashboard => this.onRemove(dashboard)} + /> +
    +
    + ); + } +} + +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)); diff --git a/public/app/features/datasources/DataSourcePermissions.test.tsx b/public/app/features/datasources/DataSourcePermissions.test.tsx deleted file mode 100644 index 8e21e5760d4..00000000000 --- a/public/app/features/datasources/DataSourcePermissions.test.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DataSourcePermissions, Props } from './DataSourcePermissions'; -import { DataSourcePermission, DataSourcePermissionDTO } from 'app/types'; -import { AclTarget, dashboardPermissionLevels } from '../../types/acl'; - -const setup = (propOverrides?: object) => { - const props: Props = { - dataSourcePermission: {} as DataSourcePermissionDTO, - pageId: 1, - addDataSourcePermission: jest.fn(), - enableDataSourcePermissions: jest.fn(), - disableDataSourcePermissions: jest.fn(), - loadDataSourcePermissions: jest.fn(), - removeDataSourcePermission: jest.fn(), - }; - - Object.assign(props, propOverrides); - - const wrapper = shallow(); - const instance = wrapper.instance() as DataSourcePermissions; - - return { - wrapper, - instance, - }; -}; - -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render permissions enabled', () => { - const { wrapper } = setup({ - dataSourcePermission: { - enabled: true, - datasourceId: 1, - permissions: [] as DataSourcePermission[], - }, - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe('Functions', () => { - describe('on add permissions', () => { - const { instance } = setup(); - - it('should add permissions for team', () => { - const mockState = { - permission: dashboardPermissionLevels[0].value, - teamId: 1, - type: AclTarget.Team, - }; - - instance.onAddPermission(mockState); - - expect(instance.props.addDataSourcePermission).toHaveBeenCalledWith(1, { teamId: 1, permission: 1 }); - }); - - it('should add permissions for user', () => { - const mockState = { - permission: dashboardPermissionLevels[0].value, - userId: 1, - type: AclTarget.User, - }; - - instance.onAddPermission(mockState); - - expect(instance.props.addDataSourcePermission).toHaveBeenCalledWith(1, { userId: 1, permission: 1 }); - }); - }); -}); diff --git a/public/app/features/datasources/DataSourcePermissions.tsx b/public/app/features/datasources/DataSourcePermissions.tsx deleted file mode 100644 index 1ea20c97933..00000000000 --- a/public/app/features/datasources/DataSourcePermissions.tsx +++ /dev/null @@ -1,155 +0,0 @@ -import React, { PureComponent } from 'react'; -import { connect } from 'react-redux'; -import SlideDown from '../../core/components/Animations/SlideDown'; -import AddDataSourcePermissions from './AddDataSourcePermissions'; -import DataSourcePermissionsList from './DataSourcePermissionsList'; -import { AclTarget } from 'app/types/acl'; -import { - addDataSourcePermission, - disableDataSourcePermissions, - enableDataSourcePermissions, - loadDataSourcePermissions, - removeDataSourcePermission, -} from './state/actions'; -import { DataSourcePermissionDTO } from 'app/types'; -import { getRouteParamsId } from '../../core/selectors/location'; - -export interface Props { - dataSourcePermission: DataSourcePermissionDTO; - pageId: number; - addDataSourcePermission: typeof addDataSourcePermission; - enableDataSourcePermissions: typeof enableDataSourcePermissions; - disableDataSourcePermissions: typeof disableDataSourcePermissions; - loadDataSourcePermissions: typeof loadDataSourcePermissions; - removeDataSourcePermission: typeof removeDataSourcePermission; -} - -interface State { - isAdding: boolean; -} - -export class DataSourcePermissions extends PureComponent { - state = { - isAdding: false, - }; - - componentDidMount() { - this.fetchDataSourcePermissions(); - } - - async fetchDataSourcePermissions() { - const { pageId, loadDataSourcePermissions } = this.props; - - return await loadDataSourcePermissions(pageId); - } - - onOpenAddPermissions = () => { - this.setState({ - isAdding: true, - }); - }; - - onEnablePermissions = () => { - const { pageId, enableDataSourcePermissions } = this.props; - enableDataSourcePermissions(pageId); - }; - - onDisablePermissions = () => { - const { pageId, disableDataSourcePermissions } = this.props; - - disableDataSourcePermissions(pageId); - }; - - onAddPermission = state => { - const { pageId, addDataSourcePermission } = this.props; - const data = { - permission: state.permission, - }; - - if (state.type === AclTarget.Team) { - addDataSourcePermission(pageId, Object.assign(data, { teamId: state.teamId })); - } else if (state.type === AclTarget.User) { - addDataSourcePermission(pageId, Object.assign(data, { userId: state.userId })); - } - }; - - onRemovePermission = item => { - this.props.removeDataSourcePermission(item.datasourceId, item.id); - }; - - onCancelAddPermission = () => { - this.setState({ - isAdding: false, - }); - }; - - render() { - const { dataSourcePermission } = this.props; - const { isAdding } = this.state; - const isPermissionsEnabled = dataSourcePermission.enabled; - - return ( -
    -
    -

    Permissions

    -
    - {isPermissionsEnabled && [ - , - , - ]} -
    - {!isPermissionsEnabled ? ( -
    -
    {'Permissions not enabled for this data source.'}
    - -
    - ProTip:{' '} - {'Only admins will be able to query the data source after you enable permissions.'} -
    -
    - ) : ( -
    - - this.onAddPermission(state)} - onCancel={this.onCancelAddPermission} - /> - - this.onRemovePermission(item)} - /> -
    - )} -
    - ); - } -} - -function mapStateToProps(state) { - return { - pageId: getRouteParamsId(state.location), - dataSourcePermission: state.dataSources.dataSourcePermission, - }; -} - -const mapDispatchToProps = { - addDataSourcePermission, - enableDataSourcePermissions, - disableDataSourcePermissions, - loadDataSourcePermissions, - removeDataSourcePermission, -}; - -export default connect(mapStateToProps, mapDispatchToProps)(DataSourcePermissions); diff --git a/public/app/features/datasources/DataSourcePermissionsList.test.tsx b/public/app/features/datasources/DataSourcePermissionsList.test.tsx deleted file mode 100644 index f89ada9ce39..00000000000 --- a/public/app/features/datasources/DataSourcePermissionsList.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { DataSourcePermissionsList, Props } from './DataSourcePermissionsList'; -import { DataSourcePermission } from '../../types'; -import { getMockDataSourcePermissionsTeam, getMockDataSourcePermissionsUser } from './__mocks__/dataSourcesMocks'; - -const setup = (propOverrides?: object) => { - const props: Props = { - items: [] as DataSourcePermission[], - onRemoveItem: jest.fn(), - }; - - Object.assign(props, propOverrides); - - return shallow(); -}; - -describe('Render', () => { - it('should render component', () => { - const wrapper = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render items', () => { - const wrapper = setup({ - items: [getMockDataSourcePermissionsUser(), getMockDataSourcePermissionsTeam()], - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/public/app/features/datasources/DataSourcePermissionsList.tsx b/public/app/features/datasources/DataSourcePermissionsList.tsx deleted file mode 100644 index 635f2cd87e8..00000000000 --- a/public/app/features/datasources/DataSourcePermissionsList.tsx +++ /dev/null @@ -1,109 +0,0 @@ -import React, { PureComponent } from 'react'; -import { DataSourcePermission } from 'app/types'; -import { dataSourceAclLevels, DataSourcePermissionLevel } from 'app/types/acl'; -import DescriptionPicker from '../../core/components/Picker/DescriptionPicker'; - -export interface Props { - items: DataSourcePermission[]; - onRemoveItem: (item) => void; -} - -export class DataSourcePermissionsList extends PureComponent { - renderAvatar(item) { - if (item.teamId) { - return ; - } else if (item.userId) { - return ; - } - - return ; - } - - renderDescription(item) { - if (item.userId) { - return [ - {item.userLogin} , - - (User) - , - ]; - } - if (item.teamId) { - return [ - {item.team} , - - (Team) - , - ]; - } - return (Role); - } - - render() { - const { items } = this.props; - const permissionLevels = [...dataSourceAclLevels]; - permissionLevels.push({ value: DataSourcePermissionLevel.Admin, label: 'Admin', description: '' }); - - return ( - - - - - - - - - - {items.map((item, index) => { - return ( - - - - - - - - ); - })} - -
    - - - Admin - (Role) - - Can -
    - {}} - value={2} - disabled={true} - className={'gf-form-input--form-dropdown-right'} - /> -
    -
    - -
    {this.renderAvatar(item)}{this.renderDescription(item)} - Can -
    - {}} - value={1} - disabled={true} - className={'gf-form-input--form-dropdown-right'} - /> -
    -
    - -
    - ); - } -} - -export default DataSourcePermissionsList; diff --git a/public/app/features/datasources/EditDataSourcePage.test.tsx b/public/app/features/datasources/EditDataSourcePage.test.tsx deleted file mode 100644 index 9efbfd541fa..00000000000 --- a/public/app/features/datasources/EditDataSourcePage.test.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import { EditDataSourcePage, Props } from './EditDataSourcePage'; -import { DataSource, NavModel } from '../../types'; - -const setup = (propOverrides?: object) => { - const props: Props = { - navModel: {} as NavModel, - dataSource: {} as DataSource, - dataSourceId: 1, - pageName: '', - loadDataSource: jest.fn(), - }; - - Object.assign(props, propOverrides); - - const wrapper = shallow(); - const instance = wrapper.instance() as EditDataSourcePage; - - return { - wrapper, - instance, - }; -}; - -describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); - - expect(wrapper).toMatchSnapshot(); - }); - - it('should render permissions page', () => { - const { wrapper } = setup({ - pageName: 'permissions', - }); - - expect(wrapper).toMatchSnapshot(); - }); -}); - -describe('Functions', () => { - describe('is page valid', () => { - it('should be a valid page', () => { - const { instance } = setup(); - - expect(instance.isValidPage('permissions')).toBeTruthy(); - }); - - it('should not be a valid page', () => { - const { instance } = setup(); - - expect(instance.isValidPage('asdf')).toBeFalsy(); - }); - }); - - describe('get current page', () => { - it('should return permissions', () => { - const { instance } = setup({ - pageName: 'permissions', - }); - - expect(instance.getCurrentPage()).toEqual('permissions'); - }); - - it('should return settings if bogus route', () => { - const { instance } = setup({ - pageName: 'asdf', - }); - - expect(instance.getCurrentPage()).toEqual('settings'); - }); - }); -}); diff --git a/public/app/features/datasources/EditDataSourcePage.tsx b/public/app/features/datasources/EditDataSourcePage.tsx deleted file mode 100644 index 18eefd8da9f..00000000000 --- a/public/app/features/datasources/EditDataSourcePage.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { PureComponent } from 'react'; -import { hot } from 'react-hot-loader'; -import { connect } from 'react-redux'; -import PageHeader from '../../core/components/PageHeader/PageHeader'; -import DataSourcePermissions from './DataSourcePermissions'; -import DataSourceSettings from './DataSourceSettings'; -import { DataSource, NavModel } from 'app/types'; -import { loadDataSource } from './state/actions'; -import { getNavModel } from '../../core/selectors/navModel'; -import { getRouteParamsId, getRouteParamsPage } from '../../core/selectors/location'; -import { getDataSourceLoadingNav } from './state/navModel'; -import { getDataSource } from './state/selectors'; - -export interface Props { - navModel: NavModel; - dataSource: DataSource; - dataSourceId: number; - pageName: string; - loadDataSource: typeof loadDataSource; -} - -enum PageTypes { - Settings = 'settings', - Permissions = 'permissions', - Dashboards = 'dashboards', -} - -const fallBackPage = PageTypes.Settings; - -export class EditDataSourcePage extends PureComponent { - componentDidMount() { - this.fetchDataSource(); - } - - async fetchDataSource() { - await this.props.loadDataSource(this.props.dataSourceId); - } - - isValidPage(currentPage) { - return (Object as any).values(PageTypes).includes(currentPage); - } - - getCurrentPage() { - const currentPage = this.props.pageName; - return this.isValidPage(currentPage) ? currentPage : fallBackPage; - } - - renderPage() { - switch (this.getCurrentPage()) { - case PageTypes.Settings: - return ; - case PageTypes.Permissions: - return ; - } - - return null; - } - - render() { - const { navModel } = this.props; - - return ( -
    - -
    {this.renderPage()}
    -
    - ); - } -} - -function mapStateToProps(state) { - const pageName = getRouteParamsPage(state.location) || fallBackPage; - const dataSourceId = getRouteParamsId(state.location); - const dataSourceLoadingNav = getDataSourceLoadingNav(pageName); - - return { - navModel: getNavModel(state.navIndex, `datasource-${pageName}-${dataSourceId}`, dataSourceLoadingNav), - dataSourceId: dataSourceId, - dataSource: getDataSource(state.dataSources, dataSourceId), - pageName: pageName, - }; -} - -const mapDispatchToProps = { - loadDataSource, -}; - -export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(EditDataSourcePage)); diff --git a/public/app/features/datasources/__mocks__/dataSourcesMocks.ts b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts index 25eb182835d..755d8eef74a 100644 --- a/public/app/features/datasources/__mocks__/dataSourcesMocks.ts +++ b/public/app/features/datasources/__mocks__/dataSourcesMocks.ts @@ -1,4 +1,4 @@ -import { DataSource, DataSourcePermission } from 'app/types'; +import { DataSource } from 'app/types'; export const getMockDataSources = (amount: number): DataSource[] => { const dataSources = []; @@ -46,32 +46,3 @@ export const getMockDataSource = (): DataSource => { user: '', }; }; - -export const getMockDataSourcePermissionsUser = (): DataSourcePermission => { - return { - created: '2018-10-10T16:50:45+02:00', - datasourceId: 1, - id: 2, - permission: 1, - permissionName: 'Query', - updated: '2018-10-10T16:50:45+02:00', - userAvatarUrl: '/avatar/926aa85c6bcefa0b4deca3223f337ae1', - userEmail: 'test@test.com', - userId: 3, - userLogin: 'testUser', - }; -}; - -export const getMockDataSourcePermissionsTeam = (): DataSourcePermission => { - return { - created: '2018-10-10T16:57:09+02:00', - datasourceId: 1, - id: 6, - permission: 1, - permissionName: 'Query', - team: 'A-team', - teamAvatarUrl: '/avatar/93c0801b955cbd443a8cfa91a401d7bc', - teamId: 1, - updated: '2018-10-10T16:57:09+02:00', - }; -}; diff --git a/public/app/features/datasources/__snapshots__/DashboardsTable.test.tsx.snap b/public/app/features/datasources/__snapshots__/DashboardsTable.test.tsx.snap new file mode 100644 index 00000000000..a85cbd4ad9b --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DashboardsTable.test.tsx.snap @@ -0,0 +1,88 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + + +
    +`; + +exports[`Render should render table 1`] = ` + + + + + + + + + + + + + +
    + + + + Graphite Carbon Metrics + + + +
    + + + + Graphite Carbon Metrics + + + + +
    +`; diff --git a/public/app/features/datasources/__snapshots__/DataSourceDashboards.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourceDashboards.test.tsx.snap new file mode 100644 index 00000000000..7a4f05a227f --- /dev/null +++ b/public/app/features/datasources/__snapshots__/DataSourceDashboards.test.tsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
    + +
    + +
    +
    +`; diff --git a/public/app/features/datasources/__snapshots__/DataSourcePermissions.test.tsx.snap b/public/app/features/datasources/__snapshots__/DataSourcePermissions.test.tsx.snap deleted file mode 100644 index 2ea7bff53d9..00000000000 --- a/public/app/features/datasources/__snapshots__/DataSourcePermissions.test.tsx.snap +++ /dev/null @@ -1,92 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Render should render component 1`] = ` -
    -
    -

    - Permissions -

    -
    -
    -
    -
    - Permissions not enabled for this data source. -
    - -
    - - ProTip: - - Only admins will be able to query the data source after you enable permissions. -
    -
    -
    -`; - -exports[`Render should render permissions enabled 1`] = ` -
    -
    -

    - Permissions -

    -
    - - -
    -
    - - - - -
    -
    -`; diff --git a/public/app/features/datasources/state/actions.ts b/public/app/features/datasources/state/actions.ts index 664ed840928..bb8fce8424a 100644 --- a/public/app/features/datasources/state/actions.ts +++ b/public/app/features/datasources/state/actions.ts @@ -1,5 +1,5 @@ import { ThunkAction } from 'redux-thunk'; -import { DataSource, DataSourcePermissionDTO, Plugin, StoreState } from 'app/types'; +import { DataSource, Plugin, StoreState } from 'app/types'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; import { updateLocation, updateNavIndex, UpdateNavIndexAction } from '../../../core/actions'; @@ -11,7 +11,6 @@ export enum ActionTypes { LoadDataSourceTypes = 'LOAD_DATA_SOURCE_TYPES', LoadDataSource = 'LOAD_DATA_SOURCE', LoadDataSourceMeta = 'LOAD_DATA_SOURCE_META', - LoadDataSourcePermissions = 'LOAD_DATA_SOURCE_PERMISSIONS', SetDataSourcesSearchQuery = 'SET_DATA_SOURCES_SEARCH_QUERY', SetDataSourcesLayoutMode = 'SET_DATA_SOURCES_LAYOUT_MODE', SetDataSourceTypeSearchQuery = 'SET_DATA_SOURCE_TYPE_SEARCH_QUERY', @@ -52,11 +51,6 @@ export interface LoadDataSourceMetaAction { payload: Plugin; } -export interface LoadDataSourcePermissionsAction { - type: ActionTypes.LoadDataSourcePermissions; - payload: DataSourcePermissionDTO; -} - const dataSourcesLoaded = (dataSources: DataSource[]): LoadDataSourcesAction => ({ type: ActionTypes.LoadDataSources, payload: dataSources, @@ -77,13 +71,6 @@ const dataSourceTypesLoaded = (dataSourceTypes: Plugin[]): LoadDataSourceTypesAc payload: dataSourceTypes, }); -const dataSourcePermissionsLoaded = ( - dataSourcePermission: DataSourcePermissionDTO -): LoadDataSourcePermissionsAction => ({ - type: ActionTypes.LoadDataSourcePermissions, - payload: dataSourcePermission, -}); - export const setDataSourcesSearchQuery = (searchQuery: string): SetDataSourcesSearchQueryAction => ({ type: ActionTypes.SetDataSourcesSearchQuery, payload: searchQuery, @@ -108,8 +95,7 @@ export type Action = | SetDataSourceTypeSearchQueryAction | LoadDataSourceAction | UpdateNavIndexAction - | LoadDataSourceMetaAction - | LoadDataSourcePermissionsAction; + | LoadDataSourceMetaAction; type ThunkResult = ThunkAction; @@ -159,42 +145,6 @@ export function loadDataSourceTypes(): ThunkResult { }; } -export function loadDataSourcePermissions(id: number): ThunkResult { - return async dispatch => { - const response = await getBackendSrv().get(`/api/datasources/${id}/permissions`); - dispatch(dataSourcePermissionsLoaded(response)); - }; -} - -export function enableDataSourcePermissions(id: number): ThunkResult { - return async dispatch => { - await getBackendSrv().post(`/api/datasources/${id}/enable-permissions`, {}); - dispatch(loadDataSourcePermissions(id)); - }; -} - -export function disableDataSourcePermissions(id: number): ThunkResult { - return async dispatch => { - await getBackendSrv().post(`/api/datasources/${id}/disable-permissions`, {}); - dispatch(loadDataSourcePermissions(id)); - }; -} - -export function addDataSourcePermission(id: number, data: object): ThunkResult { - return async dispatch => { - await getBackendSrv().post(`/api/datasources/${id}/permissions`, data); - - dispatch(loadDataSourcePermissions(id)); - }; -} - -export function removeDataSourcePermission(id: number, permissionId: number): ThunkResult { - return async dispatch => { - await getBackendSrv().delete(`/api/datasources/${id}/permissions/${permissionId}`); - dispatch(loadDataSourcePermissions(id)); - }; -} - export function nameExits(dataSources, name) { return ( dataSources.filter(dataSource => { diff --git a/public/app/features/datasources/state/reducers.ts b/public/app/features/datasources/state/reducers.ts index 8c3fd91024b..9ad3c97b448 100644 --- a/public/app/features/datasources/state/reducers.ts +++ b/public/app/features/datasources/state/reducers.ts @@ -1,4 +1,4 @@ -import { DataSource, DataSourcePermissionDTO, DataSourcesState, Plugin } from 'app/types'; +import { DataSource, DataSourcesState, Plugin } from 'app/types'; import { Action, ActionTypes } from './actions'; import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector'; @@ -12,7 +12,6 @@ const initialState: DataSourcesState = { dataSourceTypeSearchQuery: '', hasFetched: false, dataSourceMeta: {} as Plugin, - dataSourcePermission: {} as DataSourcePermissionDTO, }; export const dataSourcesReducer = (state = initialState, action: Action): DataSourcesState => { @@ -37,9 +36,6 @@ export const dataSourcesReducer = (state = initialState, action: Action): DataSo case ActionTypes.LoadDataSourceMeta: return { ...state, dataSourceMeta: action.payload }; - - case ActionTypes.LoadDataSourcePermissions: - return { ...state, dataSourcePermission: action.payload }; } return state; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 5a496ea910b..4fe67d9d37b 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -644,7 +644,9 @@ export class Explore extends React.PureComponent { /> )} {supportsTable && showingTable ? ( - +
    +
    + ) : null} {supportsLogs && showingLogs ? : null} diff --git a/public/app/features/explore/PromQueryField.test.tsx b/public/app/features/explore/PromQueryField.test.tsx index c82a1cd448f..802bb695f2c 100644 --- a/public/app/features/explore/PromQueryField.test.tsx +++ b/public/app/features/explore/PromQueryField.test.tsx @@ -96,11 +96,14 @@ describe('PromQueryField typeahead handling', () => { it('returns label suggestions on label context but leaves out labels that already exist', () => { const instance = shallow( - + ).instance() as PromQueryField; - const value = Plain.deserialize('{job="foo",}'); + const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",}'); const range = value.selection.merge({ - anchorOffset: 11, + anchorOffset: 36, }); const valueWithSelection = value.change().select(range).value; const result = instance.getTypeahead({ @@ -113,6 +116,33 @@ describe('PromQueryField typeahead handling', () => { 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( + + ).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', () => { const instance = shallow( diff --git a/public/app/features/explore/PromQueryField.tsx b/public/app/features/explore/PromQueryField.tsx index 889666c5e35..442e51af987 100644 --- a/public/app/features/explore/PromQueryField.tsx +++ b/public/app/features/explore/PromQueryField.tsx @@ -111,7 +111,7 @@ export function willApplySuggestion( case 'context-label-values': { // Always add quotes and remove existing ones instead - if (!(typeaheadText.startsWith('="') || typeaheadText.startsWith('"'))) { + if (!typeaheadText.match(/^(!?=~?"|")/)) { suggestion = `"${suggestion}`; } if (getNextCharacter() !== '"') { @@ -421,7 +421,7 @@ class PromQueryField extends React.PureComponent -1; const existingKeys = parsedSelector ? parsedSelector.labelKeys : []; - if ((text && text.startsWith('=')) || _.includes(wrapperClasses, 'attr-value')) { + if ((text && text.match(/^!?=~?/)) || _.includes(wrapperClasses, 'attr-value')) { // Label values if (labelKey && this.state.labelValues[selector] && this.state.labelValues[selector][labelKey]) { const labelValues = this.state.labelValues[selector][labelKey]; @@ -571,10 +571,10 @@ class PromQueryField extends React.PureComponentLog labels ) : ( - - - - )} + + + + )}
    diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index c89893b4f28..c3c41b7ab17 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -228,7 +228,13 @@ class QueryField extends React.PureComponent void; } -interface SFCCellProps { - columnIndex: number; - onClickCell?: (columnKey: string, rowValue: string, columnIndex: number, rowIndex: number, table: TableModel) => void; - rowIndex: number; - table: TableModel; - value: string; +function prepareRows(rows, columnNames) { + return rows.map(cells => _.zipObject(columnNames, cells)); } -function Cell(props: SFCCellProps) { - const { columnIndex, rowIndex, table, value, onClickCell } = props; - const column = table.columns[columnIndex]; - if (column && column.filterable && onClickCell) { - const onClick = event => { - event.preventDefault(); - onClickCell(column.text, value, columnIndex, rowIndex, table); +export default class Table extends PureComponent { + getCellProps = (state, rowInfo, column) => { + return { + onClick: () => { + const columnKey = column.Header; + const rowValue = rowInfo.row[columnKey]; + this.props.onClickCell(columnKey, rowValue); + }, }; - return ( -
    - ); - } - return ; -} + }; -export default class Table extends PureComponent { render() { - const { className = '', data, loading, onClickCell } = this.props; + const { data, loading } = this.props; const tableModel = data || EMPTY_TABLE; - if (!loading && data && data.rows.length === 0) { - return ( -
    - - {value} - - {value}
    - - - - - - - - - - -
    Table
    The queries returned no data for a table.
    - ); - } + const columnNames = tableModel.columns.map(({ text }) => text); + const columns = tableModel.columns.map(({ filterable, text }) => ({ + Header: text, + accessor: text, + show: text !== 'Time', + Cell: row => {row.value}, + })); + const noDataText = data ? 'The queries returned no data for a table.' : ''; + return ( - - - {tableModel.columns.map(col => )} - - - {tableModel.rows.map((row, i) => ( - - {row.map((value, j) => ( - - ))} - - ))} - -
    {col.text}
    + prepareRows(data, columnNames)} + showPagination={data} + /> ); } } diff --git a/public/app/features/explore/utils/prometheus.ts b/public/app/features/explore/utils/prometheus.ts index 8c41b94d684..170c5ec8cc5 100644 --- a/public/app/features/explore/utils/prometheus.ts +++ b/public/app/features/explore/utils/prometheus.ts @@ -28,7 +28,7 @@ export const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; const selectorRegexp = /\{[^}]*?\}/; -const labelRegexp = /\b\w+="[^"\n]*?"/g; +const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics @@ -66,11 +66,8 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any // Extract clean labels to form clean selector, incomplete labels are dropped const selector = query.slice(prefixOpen, suffixClose); const labels = {}; - selector.replace(labelRegexp, match => { - const delimiterIndex = match.indexOf('='); - const key = match.slice(0, delimiterIndex); - const value = match.slice(delimiterIndex + 1, match.length); - labels[key] = value; + selector.replace(labelRegexp, (_, key, operator, value) => { + labels[key] = { value, operator }; return ''; }); @@ -78,12 +75,12 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any const metricPrefix = query.slice(0, prefixOpen); const metricMatch = metricPrefix.match(/[A-Za-z:][\w:]*$/); if (metricMatch) { - labels['__name__'] = `"${metricMatch[0]}"`; + labels['__name__'] = { value: `"${metricMatch[0]}"`, operator: '=' }; } // Build sorted selector const labelKeys = Object.keys(labels).sort(); - const cleanSelector = labelKeys.map(key => `${key}=${labels[key]}`).join(','); + const cleanSelector = labelKeys.map(key => `${key}${labels[key].operator}${labels[key].value}`).join(','); const selectorString = ['{', cleanSelector, '}'].join(''); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index b42b06f1238..e517c48bb59 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -7,13 +7,11 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import * as rangeUtil from 'app/core/utils/rangeutil'; import * as dateMath from 'app/core/utils/datemath'; import { getExploreUrl } from 'app/core/utils/explore'; - import { metricsTabDirective } from './metrics_tab'; class MetricsPanelCtrl extends PanelCtrl { scope: any; datasource: any; - datasourceName: any; $q: any; $timeout: any; contextSrv: any; @@ -45,10 +43,6 @@ class MetricsPanelCtrl extends PanelCtrl { this.scope = $scope; this.panel.datasource = this.panel.datasource || null; - if (!this.panel.targets) { - this.panel.targets = [{}]; - } - this.events.on('refresh', this.onMetricsPanelRefresh.bind(this)); this.events.on('init-edit-mode', this.onInitMetricsPanelEditMode.bind(this)); this.events.on('panel-teardown', this.onPanelTearDown.bind(this)); @@ -62,7 +56,7 @@ class MetricsPanelCtrl extends PanelCtrl { } private onInitMetricsPanelEditMode() { - this.addEditorTab('Metrics', metricsTabDirective); + this.addEditorTab('Metrics', metricsTabDirective, 1, 'fa fa-database'); this.addEditorTab('Time range', 'public/app/features/panel/partials/panelTime.html'); } @@ -291,27 +285,6 @@ class MetricsPanelCtrl extends PanelCtrl { }); } - setDatasource(datasource) { - // switching to mixed - if (datasource.meta.mixed) { - _.each(this.panel.targets, target => { - target.datasource = this.panel.datasource; - if (!target.datasource) { - target.datasource = config.defaultDatasource; - } - }); - } else if (this.datasource && this.datasource.meta.mixed) { - _.each(this.panel.targets, target => { - delete target.datasource; - }); - } - - this.panel.datasource = datasource.value; - this.datasourceName = datasource.name; - this.datasource = null; - this.refresh(); - } - getAdditionalMenuItems() { const items = []; if ( diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index 3a1d0abe1c2..f520b5eefc0 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -1,6 +1,14 @@ -import { DashboardModel } from '../dashboard/dashboard_model'; +// Libraries +import _ from 'lodash'; import Remarkable from 'remarkable'; +// Services & utils +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +// Types +import { DashboardModel } from '../dashboard/dashboard_model'; + export class MetricsTabCtrl { dsName: string; panel: any; @@ -24,6 +32,9 @@ export class MetricsTabCtrl { $scope.ctrl = this; this.panel = this.panelCtrl.panel; + this.panel.datasource = this.panel.datasource || null; + this.panel.targets = this.panel.targets || [{}]; + this.dashboard = this.panelCtrl.dashboard; this.datasources = datasourceSrv.getMetricSources(); this.panelDsValue = this.panelCtrl.panel.datasource; @@ -66,10 +77,29 @@ export class MetricsTabCtrl { } this.datasourceInstance = option.datasource; - this.panelCtrl.setDatasource(option.datasource); + this.setDatasource(option.datasource); this.updateDatasourceOptions(); } + setDatasource(datasource) { + // switching to mixed + if (datasource.meta.mixed) { + _.each(this.panel.targets, target => { + target.datasource = this.panel.datasource; + if (!target.datasource) { + target.datasource = config.defaultDatasource; + } + }); + } else if (this.datasourceInstance && this.datasourceInstance.meta.mixed) { + _.each(this.panel.targets, target => { + delete target.datasource; + }); + } + + this.panel.datasource = datasource.value; + this.panel.refresh(); + } + addMixedQuery(option) { if (!option) { return; @@ -120,3 +150,5 @@ export function metricsTabDirective() { controller: MetricsTabCtrl, }; } + +coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index e2ae5cc78a9..5e216f6b34d 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -24,10 +24,8 @@ export class PanelCtrl { $injector: any; $location: any; $timeout: any; - fullscreen: boolean; inspector: any; editModeInitiated: boolean; - editMode: any; height: any; containerHeight: any; events: Emitter; @@ -49,7 +47,6 @@ export class PanelCtrl { this.pluginName = plugin.name; } - $scope.$on('refresh', () => this.refresh()); $scope.$on('component-did-mount', () => this.panelDidMount()); $scope.$on('$destroy', () => { @@ -58,13 +55,9 @@ export class PanelCtrl { }); } - init() { - this.events.emit('panel-initialized'); - this.publishAppEvent('panel-initialized', { scope: this.$scope }); - } - panelDidMount() { this.events.emit('component-did-mount'); + this.dashboard.panelInitialized(this.panel); } renderingCompleted() { @@ -72,7 +65,7 @@ export class PanelCtrl { } refresh() { - this.events.emit('refresh', null); + this.panel.refresh(); } publishAppEvent(evtName, evt) { @@ -102,6 +95,7 @@ export class PanelCtrl { initEditMode() { this.editorTabs = []; this.addEditorTab('General', 'public/app/partials/panelgeneral.html'); + this.editModeInitiated = true; this.events.emit('init-edit-mode', null); @@ -122,14 +116,15 @@ export class PanelCtrl { route.updateParams(); } - addEditorTab(title, directiveFn, index?) { - const editorTab = { title, directiveFn }; + addEditorTab(title, directiveFn, index?, icon?) { + const editorTab = { title, directiveFn, icon }; if (_.isString(directiveFn)) { editorTab.directiveFn = () => { return { templateUrl: directiveFn }; }; } + if (index) { this.editorTabs.splice(index, 0, editorTab); } else { @@ -190,7 +185,7 @@ export class PanelCtrl { getExtendedMenu() { const menu = []; - if (!this.fullscreen && this.dashboard.meta.canEdit) { + if (!this.panel.fullscreen && this.dashboard.meta.canEdit) { menu.push({ text: 'Duplicate', click: 'ctrl.duplicate()', @@ -220,15 +215,15 @@ export class PanelCtrl { } otherPanelInFullscreenMode() { - return this.dashboard.meta.fullscreen && !this.fullscreen; + return this.dashboard.meta.fullscreen && !this.panel.fullscreen; } calculatePanelHeight() { - if (this.fullscreen) { - const docHeight = $(window).height(); - const editHeight = Math.floor(docHeight * 0.4); + if (this.panel.fullscreen) { + const docHeight = $('.react-grid-layout').height(); + const editHeight = Math.floor(docHeight * 0.35); const fullscreenHeight = Math.floor(docHeight * 0.8); - this.containerHeight = this.editMode ? editHeight : fullscreenHeight; + this.containerHeight = this.panel.isEditing ? editHeight : fullscreenHeight; } else { this.containerHeight = this.panel.gridPos.h * GRID_CELL_HEIGHT + (this.panel.gridPos.h - 1) * GRID_CELL_VMARGIN; } @@ -237,6 +232,11 @@ export class PanelCtrl { this.containerHeight = $(window).height(); } + // hacky solution + if (this.panel.isEditing && !this.editModeInitiated) { + this.initEditMode(); + } + this.height = this.containerHeight - (PANEL_BORDER + TITLE_HEIGHT); } @@ -247,9 +247,6 @@ export class PanelCtrl { duplicate() { this.dashboard.duplicatePanel(this.panel); - this.$timeout(() => { - this.$scope.$root.$broadcast('render'); - }); } removePanel() { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 8b742e17952..77ebf754b3a 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -6,48 +6,53 @@ import baron from 'baron'; const module = angular.module('grafana.directives'); const panelTemplate = ` -
    -
    - - - - +
    +
    +
    +
    + + + + - - - + + + - -
    + +
    -
    - -
    -
    - -
    -
    -
    -

    - {{ctrl.pluginName}} -

    - - - - +
    + +
    +
    -
    -
    - +
    +
    +
    +

    + {{ctrl.pluginName}} +

    + + + + +
    + +
    +
    + +
    @@ -85,10 +90,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.dashboard.setPanelFocus(0); } - function panelHeightUpdated() { - panelContent.css({ height: ctrl.height + 'px' }); - } - function resizeScrollableContent() { if (panelScrollbar) { panelScrollbar.update(); @@ -133,7 +134,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { ctrl.events.on('panel-size-changed', () => { ctrl.calculatePanelHeight(); - panelHeightUpdated(); $timeout(() => { resizeScrollableContent(); ctrl.render(); @@ -142,7 +142,6 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { // set initial height ctrl.calculatePanelHeight(); - panelHeightUpdated(); ctrl.events.on('render', () => { if (transparentLastState !== ctrl.panel.transparent) { diff --git a/public/app/features/panel/panel_editor_tab.ts b/public/app/features/panel/panel_editor_tab.ts index 13a9369856a..f7e1c48a323 100644 --- a/public/app/features/panel/panel_editor_tab.ts +++ b/public/app/features/panel/panel_editor_tab.ts @@ -1,6 +1,7 @@ import angular from 'angular'; const directiveModule = angular.module('grafana.directives'); +const directiveCache = {}; /** @ngInject */ function panelEditorTab(dynamicDirectiveSrv) { @@ -12,17 +13,24 @@ function panelEditorTab(dynamicDirectiveSrv) { }, directive: scope => { const pluginId = scope.ctrl.pluginId; - const tabIndex = scope.index; - // create a wrapper for directiveFn - // required for metrics tab directive - // that is the same for many panels but - // given different names in this function - const fn = () => scope.editorTab.directiveFn(); + const tabName = scope.editorTab.title.toLowerCase().replace(' ', '-'); - return Promise.resolve({ - name: `panel-editor-tab-${pluginId}${tabIndex}`, - fn: fn, - }); + if (directiveCache[pluginId]) { + if (directiveCache[pluginId][tabName]) { + return directiveCache[pluginId][tabName]; + } + } else { + directiveCache[pluginId] = []; + } + + const result = { + fn: () => scope.editorTab.directiveFn(), + name: `panel-editor-tab-${pluginId}${tabName}`, + }; + + directiveCache[pluginId][tabName] = result; + + return result; }, }); } diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 5fa20c4714b..1d29d04ad98 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -8,21 +8,6 @@ const template = ` {{ctrl.timeInfo}} diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html index 0ee1f81b0c3..815a99d6b74 100644 --- a/public/app/features/panel/partials/metrics_tab.html +++ b/public/app/features/panel/partials/metrics_tab.html @@ -1,11 +1,7 @@
    - - { + this.dashboard.changePanelType(this.panelCtrl.panel, plugin.id); + }; +} + +const template = ` +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    `; + +/** @ngInject */ +export function vizTabDirective() { + 'use strict'; + return { + restrict: 'E', + template: template, + controller: VizTabCtrl, + }; +} + +react2AngularDirective('vizTypePicker', VizTypePicker, ['currentType', ['onTypeChanged', { watchDepth: 'reference' }]]); +coreModule.directive('vizTab', vizTabDirective); diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index d164a6d4255..c9fb250266c 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -2,6 +2,5 @@ import './plugin_edit_ctrl'; import './plugin_page_ctrl'; import './import_list/import_list'; import './ds_edit_ctrl'; -import './ds_dashboards_ctrl'; import './datasource_srv'; import './plugin_component'; diff --git a/public/app/features/plugins/built_in_plugins.ts b/public/app/features/plugins/built_in_plugins.ts index e29e1709ccf..b9779190a8b 100644 --- a/public/app/features/plugins/built_in_plugins.ts +++ b/public/app/features/plugins/built_in_plugins.ts @@ -14,6 +14,8 @@ import * as testDataDSPlugin from 'app/plugins/datasource/testdata/module'; import * as stackdriverPlugin from 'app/plugins/datasource/stackdriver/module'; import * as textPanel from 'app/plugins/panel/text/module'; +import * as text2Panel from 'app/plugins/panel/text2/module'; +import * as graph2Panel from 'app/plugins/panel/graph2/module'; import * as graphPanel from 'app/plugins/panel/graph/module'; import * as dashListPanel from 'app/plugins/panel/dashlist/module'; import * as pluginsListPanel from 'app/plugins/panel/pluginlist/module'; @@ -40,6 +42,8 @@ const builtInPlugins = { 'app/plugins/datasource/stackdriver/module': stackdriverPlugin, 'app/plugins/panel/text/module': textPanel, + 'app/plugins/panel/text2/module': text2Panel, + 'app/plugins/panel/graph2/module': graph2Panel, 'app/plugins/panel/graph/module': graphPanel, 'app/plugins/panel/dashlist/module': dashListPanel, 'app/plugins/panel/pluginlist/module': pluginsListPanel, diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index 7ef82519668..71a417a882f 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -1,8 +1,14 @@ +// Libraries import _ from 'lodash'; import coreModule from 'app/core/core_module'; + +// Utils import config from 'app/core/config'; import { importPluginModule } from './plugin_loader'; +// Types +import { DataSourceApi } from 'app/types/series'; + export class DatasourceSrv { datasources: any; @@ -15,7 +21,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name?) { + get(name?): Promise { if (!name) { return this.get(config.defaultDatasource); } @@ -162,5 +168,15 @@ export class DatasourceSrv { } } +let singleton: DatasourceSrv; + +export function setDatasourceSrv(srv: DatasourceSrv) { + singleton = srv; +} + +export function getDatasourceSrv(): DatasourceSrv { + return singleton; +} + coreModule.service('datasourceSrv', DatasourceSrv); export default DatasourceSrv; diff --git a/public/app/features/plugins/partials/ds_dashboards.html b/public/app/features/plugins/partials/ds_dashboards.html deleted file mode 100644 index a442e37d1f6..00000000000 --- a/public/app/features/plugins/partials/ds_dashboards.html +++ /dev/null @@ -1,7 +0,0 @@ - - -
    - - - -
    diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 41d1b6f1deb..142eb942a30 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -8,7 +8,7 @@ import { importPluginModule } from './plugin_loader'; import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module'; /** @ngInject */ -function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) { +function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache, $timeout) { function getTemplate(component) { if (component.template) { return $q.when(component.template); @@ -95,7 +95,7 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ PanelCtrl.templatePromise = getTemplate(PanelCtrl).then(template => { PanelCtrl.templateUrl = null; - PanelCtrl.template = `${template}`; + PanelCtrl.template = `${template}`; return componentInfo; }); @@ -207,10 +207,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ // let a binding digest cycle complete before adding to dom setTimeout(() => { - elem.append(child); scope.$applyAsync(() => { - scope.$broadcast('component-did-mount'); - scope.$broadcast('refresh'); + elem.append(child); + setTimeout(() => { + scope.$applyAsync(() => { + scope.$broadcast('component-did-mount'); + }); + }); }); }); } @@ -245,7 +248,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ registerPluginComponent(scope, elem, attrs, componentInfo); }) .catch(err => { - $rootScope.appEvent('alert-error', ['Plugin Error', err.message || err]); console.log('Plugin component error', err); }); }, diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index bc3c719917c..8e0958f6c1b 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -18,6 +18,7 @@ import config from 'app/core/config'; import TimeSeries from 'app/core/time_series2'; import TableModel from 'app/core/table_model'; import { coreModule, appEvents, contextSrv } from 'app/core/core'; +import { PluginExports } from 'app/types/plugins'; import * as datemath from 'app/core/utils/datemath'; import * as fileExport from 'app/core/utils/file_export'; import * as flatten from 'app/core/utils/flatten'; @@ -140,11 +141,12 @@ const flotDeps = [ 'jquery.flot.events', 'jquery.flot.gauge', ]; + for (const flotDep of flotDeps) { exposeToPlugin(flotDep, { fakeDep: 1 }); } -export function importPluginModule(path: string): Promise { +export function importPluginModule(path: string): Promise { const builtIn = builtInPlugins[path]; if (builtIn) { return Promise.resolve(builtIn); diff --git a/public/app/features/plugins/state/actions.ts b/public/app/features/plugins/state/actions.ts index dcfd510ffa0..9f53f3c1fd0 100644 --- a/public/app/features/plugins/state/actions.ts +++ b/public/app/features/plugins/state/actions.ts @@ -2,9 +2,11 @@ import { Plugin, StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; import { getBackendSrv } from '../../../core/services/backend_srv'; import { LayoutMode } from '../../../core/components/LayoutSelector/LayoutSelector'; +import { PluginDashboard } from '../../../types/plugins'; export enum ActionTypes { LoadPlugins = 'LOAD_PLUGINS', + LoadPluginDashboards = 'LOAD_PLUGIN_DASHBOARDS', SetPluginsSearchQuery = 'SET_PLUGIN_SEARCH_QUERY', SetLayoutMode = 'SET_LAYOUT_MODE', } @@ -14,6 +16,11 @@ export interface LoadPluginsAction { payload: Plugin[]; } +export interface LoadPluginDashboardsAction { + type: ActionTypes.LoadPluginDashboards; + payload: PluginDashboard[]; +} + export interface SetPluginsSearchQueryAction { type: ActionTypes.SetPluginsSearchQuery; payload: string; @@ -39,7 +46,12 @@ const pluginsLoaded = (plugins: Plugin[]): LoadPluginsAction => ({ payload: plugins, }); -export type Action = LoadPluginsAction | SetPluginsSearchQueryAction | SetLayoutModeAction; +const pluginDashboardsLoaded = (dashboards: PluginDashboard[]): LoadPluginDashboardsAction => ({ + type: ActionTypes.LoadPluginDashboards, + payload: dashboards, +}); + +export type Action = LoadPluginsAction | LoadPluginDashboardsAction | SetPluginsSearchQueryAction | SetLayoutModeAction; type ThunkResult = ThunkAction; @@ -49,3 +61,12 @@ export function loadPlugins(): ThunkResult { dispatch(pluginsLoaded(result)); }; } + +export function loadPluginDashboards(): ThunkResult { + return async (dispatch, getStore) => { + const dataSourceType = getStore().dataSources.dataSource.type; + + const response = await getBackendSrv().get(`api/plugins/${dataSourceType}/dashboards`); + dispatch(pluginDashboardsLoaded(response)); + }; +} diff --git a/public/app/features/plugins/state/reducers.ts b/public/app/features/plugins/state/reducers.ts index bd99d2029f5..0d464ef1772 100644 --- a/public/app/features/plugins/state/reducers.ts +++ b/public/app/features/plugins/state/reducers.ts @@ -1,12 +1,14 @@ import { Action, ActionTypes } from './actions'; import { Plugin, PluginsState } from 'app/types'; import { LayoutModes } from '../../../core/components/LayoutSelector/LayoutSelector'; +import { PluginDashboard } from '../../../types/plugins'; export const initialState: PluginsState = { plugins: [] as Plugin[], searchQuery: '', layoutMode: LayoutModes.Grid, hasFetched: false, + dashboards: [] as PluginDashboard[], }; export const pluginsReducer = (state = initialState, action: Action): PluginsState => { @@ -19,6 +21,9 @@ export const pluginsReducer = (state = initialState, action: Action): PluginsSta case ActionTypes.SetLayoutMode: return { ...state, layoutMode: action.payload }; + + case ActionTypes.LoadPluginDashboards: + return { ...state, dashboards: action.payload }; } return state; }; diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index 359d5b79a38..3df6ccb8b5b 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -1,5 +1,6 @@ import '../all'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import moment from 'moment'; import $q from 'q'; @@ -56,10 +57,12 @@ describe('VariableSrv', function(this: any) { return getVarMockConstructor(ctr, model, ctx); }; - ctx.variableSrv.init({ - templating: { list: [] }, - updateSubmenuVisibility: () => {}, - }); + ctx.variableSrv.init( + new DashboardModel({ + templating: { list: [] }, + updateSubmenuVisibility: () => {}, + }) + ); scenario.variable = ctx.variableSrv.createVariableFromModel(scenario.variableModel); ctx.variableSrv.addVariable(scenario.variable); diff --git a/public/app/features/templating/specs/variable_srv_init.test.ts b/public/app/features/templating/specs/variable_srv_init.test.ts index b5d00a5289e..bda5b6aa577 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -2,6 +2,7 @@ import '../all'; import _ from 'lodash'; import { VariableSrv } from '../variable_srv'; +import { DashboardModel } from '../../dashboard/dashboard_model'; import $q from 'q'; describe('VariableSrv init', function(this: any) { @@ -56,9 +57,9 @@ describe('VariableSrv init', function(this: any) { ctx.variableSrv.datasourceSrv = ctx.datasourceSrv; ctx.variableSrv.$location.search = () => scenario.urlParams; - ctx.variableSrv.dashboard = { + ctx.variableSrv.dashboard = new DashboardModel({ templating: { list: scenario.variables }, - }; + }); await ctx.variableSrv.init(ctx.variableSrv.dashboard); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 8c0f1f11f77..a676f9c2848 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -1,5 +1,8 @@ +// Libaries import angular from 'angular'; import _ from 'lodash'; + +// Utils & Services import coreModule from 'app/core/core_module'; import { variableTypes } from './variable'; import { Graph } from 'app/core/utils/dag'; @@ -10,13 +13,12 @@ export class VariableSrv { /** @ngInject */ constructor(private $rootScope, private $q, private $location, private $injector, private templateSrv) { - // update time variant variables - $rootScope.$on('refresh', this.onDashboardRefresh.bind(this), $rootScope); $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); } init(dashboard) { this.dashboard = dashboard; + this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); @@ -39,11 +41,7 @@ export class VariableSrv { }); } - onDashboardRefresh(evt, payload) { - if (payload && payload.fromVariableValueUpdated) { - return Promise.resolve({}); - } - + onTimeRangeUpdated() { const promises = this.variables.filter(variable => variable.refresh === 2).map(variable => { const previousOptions = variable.options.slice(); @@ -54,7 +52,9 @@ export class VariableSrv { }); }); - return this.$q.all(promises); + return this.$q.all(promises).then(() => { + this.dashboard.startRefresh(); + }); } processVariable(variable, queryParams) { @@ -133,7 +133,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { this.$rootScope.$emit('template-variable-value-updated'); - this.$rootScope.$broadcast('refresh', { fromVariableValueUpdated: true }); + this.dashboard.startRefresh(); } }); } @@ -291,9 +291,11 @@ export class VariableSrv { createGraph() { const g = new Graph(); - this.variables.forEach(v1 => { - g.createNode(v1.name); + this.variables.forEach(v => { + g.createNode(v.name); + }); + this.variables.forEach(v1 => { this.variables.forEach(v2 => { if (v1 === v2) { return; diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html index 9506587c515..32acdc435f2 100644 --- a/public/app/partials/dashboard.html +++ b/public/app/partials/dashboard.html @@ -7,12 +7,11 @@ class="dashboard-settings"> -
    +
    - - +
    diff --git a/public/app/plugins/datasource/cloudwatch/config_ctrl.ts b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts index ff0d39944ca..6fe48cb1715 100644 --- a/public/app/plugins/datasource/cloudwatch/config_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/config_ctrl.ts @@ -1,17 +1,21 @@ +import _ from 'lodash'; export class CloudWatchConfigCtrl { static templateUrl = 'partials/config.html'; current: any; + datasourceSrv: any; accessKeyExist = false; secretKeyExist = false; /** @ngInject */ - constructor($scope) { + constructor($scope, datasourceSrv) { this.current.jsonData.timeField = this.current.jsonData.timeField || '@timestamp'; this.current.jsonData.authType = this.current.jsonData.authType || 'credentials'; this.accessKeyExist = this.current.secureJsonFields.accessKey; this.secretKeyExist = this.current.secureJsonFields.secretKey; + this.datasourceSrv = datasourceSrv; + this.getRegions(); } resetAccessKey() { @@ -36,4 +40,47 @@ export class CloudWatchConfigCtrl { { name: 'Monthly', value: 'Monthly', example: '[logstash-]YYYY.MM' }, { name: 'Yearly', value: 'Yearly', example: '[logstash-]YYYY' }, ]; + + regions = [ + 'ap-northeast-1', + 'ap-northeast-2', + 'ap-northeast-3', + 'ap-south-1', + 'ap-southeast-1', + 'ap-southeast-2', + 'ca-central-1', + 'cn-north-1', + 'cn-northwest-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-gov-east-1', + 'us-gov-west-1', + 'us-iso-east-1', + 'us-isob-east-1', + 'us-west-1', + 'us-west-2', + ]; + + getRegions() { + this.datasourceSrv + .loadDatasource(this.current.name) + .then(ds => { + return ds.getRegions(); + }) + .then( + regions => { + this.regions = _.map(regions, 'value'); + }, + err => { + console.error('failed to get latest regions'); + } + ); + } } diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 7821c5dcb2c..b4f739f934c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -137,7 +137,11 @@ export default class CloudWatchDatasource { if (res.results) { _.forEach(res.results, queryRes => { _.forEach(queryRes.series, series => { - data.push({ target: series.name, datapoints: series.points, unit: queryRes.meta.unit || 'none' }); + const s = { target: series.name, datapoints: series.points } as any; + if (queryRes.meta.unit) { + s.unit = queryRes.meta.unit; + } + data.push(s); }); }); } diff --git a/public/app/plugins/datasource/cloudwatch/partials/config.html b/public/app/plugins/datasource/cloudwatch/partials/config.html index e5ab0910cba..40249d32b7e 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/config.html +++ b/public/app/plugins/datasource/cloudwatch/partials/config.html @@ -39,7 +39,7 @@
    - + Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. diff --git a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts index 4f4b2961761..ba5a39688b3 100644 --- a/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts +++ b/public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.ts @@ -1,4 +1,5 @@ import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; export class CloudWatchQueryParameter { @@ -239,5 +240,5 @@ export class CloudWatchQueryParameterCtrl { } } -angular.module('grafana.controllers').directive('cloudwatchQueryParameter', CloudWatchQueryParameter); -angular.module('grafana.controllers').controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); +coreModule.directive('cloudwatchQueryParameter', CloudWatchQueryParameter); +coreModule.controller('CloudWatchQueryParameterCtrl', CloudWatchQueryParameterCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts index 8963f2c3f4b..cacf86201fe 100644 --- a/public/app/plugins/datasource/elasticsearch/bucket_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/bucket_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -226,6 +226,5 @@ export class ElasticBucketAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticBucketAgg', elasticBucketAgg); -module.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); +coreModule.directive('elasticBucketAgg', elasticBucketAgg); +coreModule.controller('ElasticBucketAggCtrl', ElasticBucketAggCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 623eed68914..1dd0d892360 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; @@ -203,6 +203,5 @@ export class ElasticMetricAggCtrl { } } -const module = angular.module('grafana.directives'); -module.directive('elasticMetricAgg', elasticMetricAgg); -module.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); +coreModule.directive('elasticMetricAgg', elasticMetricAgg); +coreModule.controller('ElasticMetricAggCtrl', ElasticMetricAggCtrl); diff --git a/public/app/plugins/datasource/graphite/add_graphite_func.ts b/public/app/plugins/datasource/graphite/add_graphite_func.ts index a5c1dc49959..ea3dfe8ff5e 100644 --- a/public/app/plugins/datasource/graphite/add_graphite_func.ts +++ b/public/app/plugins/datasource/graphite/add_graphite_func.ts @@ -1,8 +1,8 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; import Drop from 'tether-drop'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteAddFunc($compile) { @@ -130,7 +130,7 @@ export function graphiteAddFunc($compile) { }; } -angular.module('grafana.directives').directive('graphiteAddFunc', graphiteAddFunc); +coreModule.directive('graphiteAddFunc', graphiteAddFunc); function createFunctionDropDownMenu(funcDefs) { const categories = {}; diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 68cc6f1452e..9e19083a9c3 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -1,7 +1,7 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import rst2html from 'rst2html'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { @@ -315,4 +315,4 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { }; } -angular.module('grafana.directives').directive('graphiteFuncEditor', graphiteFuncEditor); +coreModule.directive('graphiteFuncEditor', graphiteFuncEditor); diff --git a/public/app/plugins/datasource/mssql/config_ctrl.ts b/public/app/plugins/datasource/mssql/config_ctrl.ts new file mode 100644 index 00000000000..c80d657a914 --- /dev/null +++ b/public/app/plugins/datasource/mssql/config_ctrl.ts @@ -0,0 +1,10 @@ +export class MssqlConfigCtrl { + static templateUrl = 'partials/config.html'; + + current: any; + + /** @ngInject */ + constructor($scope) { + this.current.jsonData.encrypt = this.current.jsonData.encrypt || 'false'; + } +} diff --git a/public/app/plugins/datasource/mssql/module.ts b/public/app/plugins/datasource/mssql/module.ts index 478ecadcb3e..bf46b6d0947 100644 --- a/public/app/plugins/datasource/mssql/module.ts +++ b/public/app/plugins/datasource/mssql/module.ts @@ -1,9 +1,6 @@ import { MssqlDatasource } from './datasource'; import { MssqlQueryCtrl } from './query_ctrl'; - -class MssqlConfigCtrl { - static templateUrl = 'partials/config.html'; -} +import { MssqlConfigCtrl } from './config_ctrl'; const defaultQuery = `SELECT as time, diff --git a/public/app/plugins/datasource/mssql/partials/config.html b/public/app/plugins/datasource/mssql/partials/config.html index 4cb42e90f7c..db76f60e5e3 100644 --- a/public/app/plugins/datasource/mssql/partials/config.html +++ b/public/app/plugins/datasource/mssql/partials/config.html @@ -27,6 +27,22 @@ reset
    + +
    + +
    + + + Determines whether or to which extent a secure SSL TCP/IP connection will be negotiated with the server. +
      +
    • disable - Data sent between client and server is not encrypted.
    • +
    • false - Data sent between client and server is not encrypted beyond the login packet. (default)
    • +
    • true - Data sent between client and server is encrypted.
    • +
    + 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. +
    +
    +
    Connection limits diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index f1db05cabe8..13948c5d793 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -20,7 +20,7 @@ export class PostgresDatasource { this.interval = (instanceSettings.jsonData || {}).timeInterval; } - interpolateVariable(value, variable) { + interpolateVariable = (value, variable) => { if (typeof value === 'string') { if (variable.multi || variable.includeAll) { return this.queryModel.quoteLiteral(value); @@ -37,7 +37,7 @@ export class PostgresDatasource { return this.queryModel.quoteLiteral(v); }); return quotedValues.join(','); - } + }; query(options) { const queries = _.filter(options.targets, target => { diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index feada28deea..680f7a8fb98 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -12,7 +12,7 @@ export default class PrometheusMetricFindQuery { } process() { - const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]+)\)\s*$/; + const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/; const metricNamesRegex = /^metrics\((.+)\)\s*$/; const queryResultRegex = /^query_result\((.+)\)\s*$/; diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 7ea748e1082..b77abdbdab3 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -89,7 +89,7 @@ export default class StackdriverDatasource { } resolvePanelUnitFromTargets(targets: any[]) { - let unit = 'none'; + let unit; if (targets.length > 0 && targets.every(t => t.unit === targets[0].unit)) { if (stackdriverUnitMappings.hasOwnProperty(targets[0].unit)) { unit = stackdriverUnitMappings[targets[0].unit]; @@ -109,13 +109,16 @@ export default class StackdriverDatasource { const unit = this.resolvePanelUnitFromTargets(options.targets); queryRes.series.forEach(series => { - result.push({ + let timeSerie: any = { target: series.name, datapoints: series.points, refId: queryRes.refId, meta: queryRes.meta, - unit, - }); + }; + if (unit) { + timeSerie = { ...timeSerie, unit }; + } + result.push(timeSerie); }); }); } diff --git a/public/app/plugins/datasource/stackdriver/filter_segments.ts b/public/app/plugins/datasource/stackdriver/filter_segments.ts index 8d193f663c2..9eb27f31975 100644 --- a/public/app/plugins/datasource/stackdriver/filter_segments.ts +++ b/public/app/plugins/datasource/stackdriver/filter_segments.ts @@ -87,7 +87,7 @@ export class FilterSegments { } // remove condition if it is first segment - if (index === 0 && this.filterSegments[0].type === 'condition') { + if (index === 0 && this.filterSegments.length > 0 && this.filterSegments[0].type === 'condition') { this.filterSegments.splice(0, 1); } } diff --git a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts index 98a1258cb15..6cd6c805463 100644 --- a/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_aggregation_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as options from './constants'; import kbn from 'app/core/utils/kbn'; @@ -83,5 +83,5 @@ export class StackdriverAggregationCtrl { } } -angular.module('grafana.controllers').directive('stackdriverAggregation', StackdriverAggregation); -angular.module('grafana.controllers').controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); +coreModule.directive('stackdriverAggregation', StackdriverAggregation); +coreModule.controller('StackdriverAggregationCtrl', StackdriverAggregationCtrl); diff --git a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts index 786b2831e89..7af76720d23 100644 --- a/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_filter_ctrl.ts @@ -1,4 +1,4 @@ -import angular from 'angular'; +import coreModule from 'app/core/core_module'; import _ from 'lodash'; import { FilterSegments, DefaultRemoveFilterValue } from './filter_segments'; import appEvents from 'app/core/app_events'; @@ -281,5 +281,5 @@ export class StackdriverFilterCtrl { } } -angular.module('grafana.controllers').directive('stackdriverFilter', StackdriverFilter); -angular.module('grafana.controllers').controller('StackdriverFilterCtrl', StackdriverFilterCtrl); +coreModule.directive('stackdriverFilter', StackdriverFilter); +coreModule.controller('StackdriverFilterCtrl', StackdriverFilterCtrl); diff --git a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts index 3117be402a9..ab0c0653816 100644 --- a/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts +++ b/public/app/plugins/datasource/stackdriver/specs/datasource.test.ts @@ -235,8 +235,8 @@ describe('StackdriverDataSource', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }]); }); - it('should return none', () => { - expect(res).toEqual('none'); + it('should return undefined', () => { + expect(res).toBeUndefined(); }); }); describe('and the stackdriver unit has a corresponding grafana unit', () => { @@ -262,16 +262,16 @@ describe('StackdriverDataSource', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'megaseconds' }, { unit: 'megaseconds' }]); }); - it('should return the default value - none', () => { - expect(res).toEqual('none'); + it('should return the default value of undefined', () => { + expect(res).toBeUndefined(); }); }); describe('and all target units are not the same', () => { beforeEach(() => { res = ds.resolvePanelUnitFromTargets([{ unit: 'bit' }, { unit: 'min' }]); }); - it('should return the default value - none', () => { - expect(res).toEqual('none'); + it('should return the default value of undefined', () => { + expect(res).toBeUndefined(); }); }); }); diff --git a/public/app/plugins/datasource/testdata/datasource.ts b/public/app/plugins/datasource/testdata/datasource.ts index d112e656f3f..0197626cd0b 100644 --- a/public/app/plugins/datasource/testdata/datasource.ts +++ b/public/app/plugins/datasource/testdata/datasource.ts @@ -62,7 +62,6 @@ class TestDataDatasource { }); } - console.log(res); return { data: data }; }); } diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 33db0e7220a..7a8e24539f7 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -713,7 +713,9 @@ class GraphElement { if (min && max && ticks) { const range = max - min; const secPerTick = range / ticks / 1000; - const oneDay = 86400000; + // Need have 10 milisecond margin on the day range + // As sometimes last 24 hour dashboard evaluates to more than 86400000 + const oneDay = 86400010; const oneYear = 31536000000; if (secPerTick <= 45) { diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index cf317389941..7b01c46c4d3 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -1,11 +1,9 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import baron from 'baron'; +import coreModule from 'app/core/core_module'; -const module = angular.module('grafana.directives'); - -module.directive('graphLegend', (popoverSrv, $timeout) => { +coreModule.directive('graphLegend', (popoverSrv, $timeout) => { return { link: (scope, elem) => { let firstRender = true; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index f0751ddd816..07256164c56 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -134,9 +134,9 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { this.addEditorTab('Alert', alertTab, 5); @@ -156,7 +156,16 @@ class GraphCtrl extends MetricsPanelCtrl { panel: this.panel, range: this.range, }); - return super.issueQueries(datasource); + + /* Wait for annotationSrv requests to get datasources to + * resolve before issuing queries. This allows the annotations + * service to fire annotations queries before graph queries + * (but not wait for completion). This resolves + * issue 11806. + */ + return this.annotationsSrv.datasourcePromises.then(r => { + return super.issueQueries(datasource); + }); } zoomOut(evt) { diff --git a/public/app/plugins/panel/graph/series_overrides_ctrl.ts b/public/app/plugins/panel/graph/series_overrides_ctrl.ts index deb7bd8ba61..540d19fb47a 100644 --- a/public/app/plugins/panel/graph/series_overrides_ctrl.ts +++ b/public/app/plugins/panel/graph/series_overrides_ctrl.ts @@ -1,5 +1,5 @@ import _ from 'lodash'; -import angular from 'angular'; +import coreModule from 'app/core/core_module'; /** @ngInject */ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { @@ -156,4 +156,4 @@ export function SeriesOverridesCtrl($scope, $element, popoverSrv) { $scope.updateCurrentOverrides(); } -angular.module('grafana.controllers').controller('SeriesOverridesCtrl', SeriesOverridesCtrl); +coreModule.controller('SeriesOverridesCtrl', SeriesOverridesCtrl); diff --git a/public/app/plugins/panel/graph2/README.md b/public/app/plugins/panel/graph2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/graph2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/graph2/img/icn-text-panel.svg b/public/app/plugins/panel/graph2/img/icn-text-panel.svg new file mode 100644 index 00000000000..a9d0a1d2c4a --- /dev/null +++ b/public/app/plugins/panel/graph2/img/icn-text-panel.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx new file mode 100644 index 00000000000..c2b8c355440 --- /dev/null +++ b/public/app/plugins/panel/graph2/module.tsx @@ -0,0 +1,43 @@ +// Libraries +import _ from 'lodash'; +import React, { PureComponent } from 'react'; + +// Components +import Graph from 'app/viz/Graph'; +import { getTimeSeriesVMs } from 'app/viz/state/timeSeries'; + +// Types +import { PanelProps, NullValueMode } from 'app/types'; + +interface Options { + showBars: boolean; +} + +interface Props extends PanelProps { + options: Options; +} + +export class Graph2 extends PureComponent { + constructor(props) { + super(props); + } + + render() { + const { timeSeries, timeRange } = this.props; + + const vmSeries = getTimeSeriesVMs({ + timeSeries: timeSeries, + nullValueMode: NullValueMode.Ignore, + }); + + return ; + } +} + +export class TextOptions extends PureComponent { + render() { + return

    Text2 Options component

    ; + } +} + +export { Graph2 as PanelComponent, TextOptions as PanelOptions }; diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json new file mode 100644 index 00000000000..b519a57fae4 --- /dev/null +++ b/public/app/plugins/panel/graph2/plugin.json @@ -0,0 +1,17 @@ +{ + "type": "panel", + "name": "React Graph", + "id": "graph2", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-text-panel.svg", + "large": "img/icn-text-panel.svg" + } + } +} + diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 628186569dd..0e011e59439 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -1,12 +1,10 @@ -import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import * as d3 from 'd3'; import { contextSrv } from 'app/core/core'; import { tickStep } from 'app/core/utils/ticks'; import { getColorScale, getOpacityScale } from './color_scale'; - -const module = angular.module('grafana.directives'); +import coreModule from 'app/core/core_module'; const LEGEND_HEIGHT_PX = 6; const LEGEND_WIDTH_PX = 100; @@ -16,7 +14,7 @@ const LEGEND_VALUE_MARGIN = 0; /** * Color legend for heatmap editor. */ -module.directive('colorLegend', () => { +coreModule.directive('colorLegend', () => { return { restrict: 'E', template: '
    ', @@ -52,7 +50,7 @@ module.directive('colorLegend', () => { /** * Heatmap legend with scale values. */ -module.directive('heatmapLegend', () => { +coreModule.directive('heatmapLegend', () => { return { restrict: 'E', template: `
    `, diff --git a/public/app/plugins/panel/text2/README.md b/public/app/plugins/panel/text2/README.md new file mode 100644 index 00000000000..667ab51784a --- /dev/null +++ b/public/app/plugins/panel/text2/README.md @@ -0,0 +1,5 @@ +# Text Panel - Native Plugin + +The Text Panel is **included** with Grafana. + +The Text Panel is a very simple panel that displays text. The source text is written in the Markdown syntax meaning you can format the text. Read [GitHub's Mastering Markdown](https://guides.github.com/features/mastering-markdown/) to learn more. diff --git a/public/app/plugins/panel/text2/img/icn-graph-panel.svg b/public/app/plugins/panel/text2/img/icn-graph-panel.svg new file mode 100644 index 00000000000..463b3d5770b --- /dev/null +++ b/public/app/plugins/panel/text2/img/icn-graph-panel.svg @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx new file mode 100644 index 00000000000..b10dc8b545e --- /dev/null +++ b/public/app/plugins/panel/text2/module.tsx @@ -0,0 +1,14 @@ +import React, { PureComponent } from 'react'; +import { PanelProps } from 'app/types'; + +export class Text2 extends PureComponent { + constructor(props) { + super(props); + } + + render() { + return

    Text Panel!

    ; + } +} + +export { Text2 as PanelComponent }; diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json new file mode 100644 index 00000000000..b1133f65e36 --- /dev/null +++ b/public/app/plugins/panel/text2/plugin.json @@ -0,0 +1,19 @@ +{ + "type": "panel", + "name": "Text v2", + "id": "text2", + + "state": "alpha", + + "info": { + "author": { + "name": "Grafana Project", + "url": "https://grafana.com" + }, + "logos": { + "small": "img/icn-graph-panel.svg", + "large": "img/icn-graph-panel.svg" + } + } +} + diff --git a/public/app/core/components/grafana_app.ts b/public/app/routes/GrafanaCtrl.ts similarity index 96% rename from public/app/core/components/grafana_app.ts rename to public/app/routes/GrafanaCtrl.ts index 2774ab99426..d6291c94a6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -8,9 +8,10 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import colors from 'app/core/utils/colors'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { configureStore } from 'app/store/configureStore'; +import { TimeSrv, setTimeSrv } from 'app/features/dashboard/time_srv'; +import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; +import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -23,12 +24,15 @@ export class GrafanaCtrl { contextSrv, bridgeSrv, backendSrv: BackendSrv, + timeSrv: TimeSrv, datasourceSrv: DatasourceSrv, angularLoader: AngularLoader ) { - // sets singleston instances for angular services so react components can access them + // make angular loader service available to react components setAngularLoader(angularLoader); setBackendSrv(backendSrv); + setDatasourceSrv(datasourceSrv); + setTimeSrv(timeSrv); configureStore(); $scope.init = () => { diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 632d921103a..24cabb63a9d 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -13,7 +13,7 @@ import FolderPermissions from 'app/features/folders/FolderPermissions'; import DataSourcesListPage from 'app/features/datasources/DataSourcesListPage'; import NewDataSourcePage from '../features/datasources/NewDataSourcePage'; import UsersListPage from 'app/features/users/UsersListPage'; -import EditDataSourcePage from 'app/features/datasources/EditDataSourcePage'; +import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards'; /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { @@ -74,14 +74,9 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/datasources/edit/:id/dashboards', { - templateUrl: 'public/app/features/plugins/partials/ds_dashboards.html', - controller: 'DataSourceDashboardsCtrl', - controllerAs: 'ctrl', - }) - .when('/datasources/edit/:id/:page?', { template: '', resolve: { - component: () => EditDataSourcePage, + component: () => DataSourceDashboards, }, }) .when('/datasources/new', { diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 0a93a4baa0f..ccd027a0b6d 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -11,7 +11,7 @@ import pluginReducers from 'app/features/plugins/state/reducers'; import dataSourcesReducers from 'app/features/datasources/state/reducers'; import usersReducers from 'app/features/users/state/reducers'; -const rootReducer = combineReducers({ +const rootReducers = { ...sharedReducers, ...alertingReducers, ...teamsReducers, @@ -21,13 +21,19 @@ const rootReducer = combineReducers({ ...pluginReducers, ...dataSourcesReducers, ...usersReducers, -}); +}; export let store; +export function addRootReducer(reducers) { + Object.assign(rootReducers, ...reducers); +} + export function configureStore() { const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + const rootReducer = combineReducers(rootReducers); + if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index b9e11b691db..d98dcb3e839 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -1,28 +1,6 @@ import { LayoutMode } from '../core/components/LayoutSelector/LayoutSelector'; import { Plugin } from './plugins'; -export interface DataSourcePermission { - id: number; - datasourceId: number; - permission: number; - permissionName: string; - created: string; - updated: string; - userId?: number; - userLogin?: string; - userEmail?: string; - userAvatarUrl?: string; - teamId?: number; - teamAvatarUrl?: string; - team?: string; -} - -export interface DataSourcePermissionDTO { - datasourceId: number; - enabled: boolean; - permissions: DataSourcePermission[]; -} - export interface DataSource { id: number; orgId: number; @@ -50,8 +28,7 @@ export interface DataSourcesState { layoutMode: LayoutMode; dataSourcesCount: number; dataSourceTypes: Plugin[]; - hasFetched: boolean; dataSource: DataSource; dataSourceMeta: Plugin; - dataSourcePermission: DataSourcePermissionDTO; + hasFetched: boolean; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index eee34c291af..27c1644e6ab 100644 --- a/public/app/types/index.ts +++ b/public/app/types/index.ts @@ -7,8 +7,21 @@ import { DashboardState } from './dashboard'; import { DashboardAcl, OrgRole, PermissionLevel } from './acl'; import { ApiKey, ApiKeysState, NewApiKey } from './apiKeys'; import { Invitee, OrgUser, User, UsersState } from './user'; -import { DataSource, DataSourcePermissionDTO, DataSourcePermission, DataSourcesState } from './datasources'; -import { PluginMeta, Plugin, PluginsState } from './plugins'; +import { DataSource, DataSourcesState } from './datasources'; +import { + TimeRange, + LoadingState, + TimeSeries, + TimeSeriesVM, + TimeSeriesVMs, + TimeSeriesStats, + NullValueMode, + DataQuery, + DataQueryResponse, + DataQueryOptions, +} from './series'; +import { PanelProps } from './panel'; +import { PluginDashboard, PluginMeta, Plugin, PluginsState } from './plugins'; export { Team, @@ -41,12 +54,22 @@ export { Plugin, PluginsState, DataSourcesState, - DataSourcePermissionDTO, - DataSourcePermission, Invitee, OrgUser, User, UsersState, + TimeRange, + LoadingState, + PanelProps, + TimeSeries, + TimeSeriesVM, + TimeSeriesVMs, + NullValueMode, + TimeSeriesStats, + DataQuery, + DataQueryResponse, + DataQueryOptions, + PluginDashboard, }; export interface StoreState { diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 4a7f51523a7..7dcf57f7e02 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -2,6 +2,7 @@ export interface LocationUpdate { path?: string; query?: UrlQueryMap; routeParams?: UrlQueryMap; + partial?: boolean; } export interface LocationState { diff --git a/public/app/types/panel.ts b/public/app/types/panel.ts new file mode 100644 index 00000000000..5ece77fc5aa --- /dev/null +++ b/public/app/types/panel.ts @@ -0,0 +1,7 @@ +import { LoadingState, TimeSeries, TimeRange } from './series'; + +export interface PanelProps { + timeSeries: TimeSeries[]; + timeRange: TimeRange; + loading: LoadingState; +} diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index c7e9aa7a564..826ce2d48ec 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -1,3 +1,25 @@ +export interface PluginExports { + PanelCtrl?; + PanelComponent?: any; + Datasource?: any; + QueryCtrl?: any; + ConfigCtrl?: any; + AnnotationsQueryCtrl?: any; + PanelOptions?: any; +} + +export interface PanelPlugin { + id: string; + name: string; + meta: any; + hideFromList: boolean; + module: string; + baseUrl: string; + info: any; + sort: number; + exports?: PluginExports; +} + export interface PluginMeta { id: string; name: string; @@ -40,9 +62,26 @@ export interface Plugin { type: string; } +export interface PluginDashboard { + dashboardId: number; + description: string; + folderId: number; + imported: boolean; + importedRevision: number; + importedUri: string; + importedUrl: string; + path: string; + pluginId: string; + removed: boolean; + revision: number; + slug: string; + title: string; +} + export interface PluginsState { plugins: Plugin[]; searchQuery: string; layoutMode: string; hasFetched: boolean; + dashboards: PluginDashboard[]; } diff --git a/public/app/types/series.ts b/public/app/types/series.ts new file mode 100644 index 00000000000..5396880611b --- /dev/null +++ b/public/app/types/series.ts @@ -0,0 +1,91 @@ +import { Moment } from 'moment'; + +export enum LoadingState { + NotStarted = 'NotStarted', + Loading = 'Loading', + Done = 'Done', + Error = 'Error', +} + +export interface RawTimeRange { + from: Moment | string; + to: Moment | string; +} + +export interface TimeRange { + from: Moment; + to: Moment; + raw: RawTimeRange; +} + +export type TimeSeriesValue = string | number | null; + +export type TimeSeriesPoints = TimeSeriesValue[][]; + +export interface TimeSeries { + target: string; + datapoints: TimeSeriesPoints; + unit?: string; +} + +/** View model projection of a time series */ +export interface TimeSeriesVM { + label: string; + color: string; + data: TimeSeriesValue[][]; + stats: TimeSeriesStats; +} + +export interface TimeSeriesStats { + total: number; + max: number; + min: number; + logmin: number; + avg: number | null; + current: number | null; + first: number | null; + delta: number; + diff: number | null; + range: number | null; + timeStep: number; + count: number; + allIsNull: boolean; + allIsZero: boolean; +} + +export enum NullValueMode { + Null = 'null', + Ignore = 'connected', + AsZero = 'null as zero', +} + +/** View model projection of many time series */ +export interface TimeSeriesVMs { + [index: number]: TimeSeriesVM; +} + +export interface DataQueryResponse { + data: TimeSeries[]; +} + +export interface DataQuery { + refId: string; +} + +export interface DataQueryOptions { + timezone: string; + range: TimeRange; + rangeRaw: RawTimeRange; + targets: DataQuery[]; + panelId: number; + dashboardId: number; + cacheTimeout?: string; + interval: string; + intervalMs: number; + maxDataPoints: number; + scopedVars: object; +} + +export interface DataSourceApi { + query(options: DataQueryOptions): Promise; +} diff --git a/public/app/viz/Graph.tsx b/public/app/viz/Graph.tsx new file mode 100644 index 00000000000..fab65225715 --- /dev/null +++ b/public/app/viz/Graph.tsx @@ -0,0 +1,124 @@ +// Libraries +import $ from 'jquery'; +import React, { PureComponent } from 'react'; +import { withSize } from 'react-sizeme'; +import 'vendor/flot/jquery.flot'; +import 'vendor/flot/jquery.flot.time'; + +// Types +import { TimeRange, TimeSeriesVMs } from 'app/types'; + +// Copied from graph.ts +function time_format(ticks, min, max) { + if (min && max && ticks) { + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; + + if (secPerTick <= 45) { + return '%H:%M:%S'; + } + if (secPerTick <= 7200 || range <= oneDay) { + return '%H:%M'; + } + if (secPerTick <= 80000) { + return '%m/%d %H:%M'; + } + if (secPerTick <= 2419200 || range <= oneYear) { + return '%m/%d'; + } + return '%Y-%m'; + } + + return '%H:%M'; +} + +const FLOT_OPTIONS = { + legend: { + show: false, + }, + series: { + lines: { + linewidth: 1, + zero: false, + }, + shadowSize: 0, + }, + grid: { + minBorderMargin: 0, + markings: [], + backgroundColor: null, + borderWidth: 0, + // hoverable: true, + clickable: true, + color: '#a1a1a1', + margin: { left: 0, right: 0 }, + labelMarginX: 0, + }, +}; + +interface GraphProps { + timeSeries: TimeSeriesVMs; + timeRange: TimeRange; + size?: { width: number; height: number }; +} + +export class Graph extends PureComponent { + element: any; + + componentDidUpdate(prevProps: GraphProps) { + if ( + prevProps.timeSeries !== this.props.timeSeries || + prevProps.timeRange !== this.props.timeRange || + prevProps.size !== this.props.size + ) { + this.draw(); + } + } + + componentDidMount() { + this.draw(); + } + + draw() { + const { size, timeSeries, timeRange } = this.props; + + if (!size) { + return; + } + + const ticks = (size.width || 0) / 100; + const min = timeRange.from.valueOf(); + const max = timeRange.to.valueOf(); + + const dynamicOptions = { + xaxis: { + mode: 'time', + min: min, + max: max, + label: 'Datetime', + ticks: ticks, + timeformat: time_format(ticks, min, max), + }, + }; + + const options = { + ...FLOT_OPTIONS, + ...dynamicOptions, + }; + + console.log('plot', timeSeries, options); + $.plot(this.element, timeSeries, options); + } + + render() { + return ( +
    +
    (this.element = e)} /> +
    + ); + } +} + +export default withSize()(Graph); diff --git a/public/app/viz/state/timeSeries.ts b/public/app/viz/state/timeSeries.ts new file mode 100644 index 00000000000..e22cb4681b7 --- /dev/null +++ b/public/app/viz/state/timeSeries.ts @@ -0,0 +1,168 @@ +// Libraries +import _ from 'lodash'; + +// Utils +import colors from 'app/core/utils/colors'; + +// Types +import { TimeSeries, TimeSeriesVMs, NullValueMode } from 'app/types'; + +interface Options { + timeSeries: TimeSeries[]; + nullValueMode: NullValueMode; +} + +export function getTimeSeriesVMs({ timeSeries, nullValueMode }: Options): TimeSeriesVMs { + const vmSeries = timeSeries.map((item, index) => { + const colorIndex = index % colors.length; + const label = item.target; + const result = []; + + // stat defaults + let total = 0; + let max = -Number.MAX_VALUE; + let min = Number.MAX_VALUE; + let logmin = Number.MAX_VALUE; + let avg = null; + let current = null; + let first = null; + let delta = 0; + let diff = null; + let range = null; + let timeStep = Number.MAX_VALUE; + let allIsNull = true; + let allIsZero = true; + + const ignoreNulls = nullValueMode === NullValueMode.Ignore; + const nullAsZero = nullValueMode === NullValueMode.AsZero; + + let currentTime; + let currentValue; + let nonNulls = 0; + let previousTime; + let previousValue = 0; + let previousDeltaUp = true; + + for (let i = 0; i < item.datapoints.length; i++) { + currentValue = item.datapoints[i][0]; + currentTime = item.datapoints[i][1]; + + // Due to missing values we could have different timeStep all along the series + // so we have to find the minimum one (could occur with aggregators such as ZimSum) + if (previousTime !== undefined) { + const currentStep = currentTime - previousTime; + if (currentStep < timeStep) { + timeStep = currentStep; + } + } + + previousTime = currentTime; + + if (currentValue === null) { + if (ignoreNulls) { + continue; + } + if (nullAsZero) { + currentValue = 0; + } + } + + if (currentValue !== null) { + if (_.isNumber(currentValue)) { + total += currentValue; + allIsNull = false; + nonNulls++; + } + + if (currentValue > max) { + max = currentValue; + } + + if (currentValue < min) { + min = currentValue; + } + + if (first === null) { + first = currentValue; + } else { + if (previousValue > currentValue) { + // counter reset + previousDeltaUp = false; + if (i === item.datapoints.length - 1) { + // reset on last + delta += currentValue; + } + } else { + if (previousDeltaUp) { + delta += currentValue - previousValue; // normal increment + } else { + delta += currentValue; // account for counter reset + } + previousDeltaUp = true; + } + } + previousValue = currentValue; + + if (currentValue < logmin && currentValue > 0) { + logmin = currentValue; + } + + if (currentValue !== 0) { + allIsZero = false; + } + } + + result.push([currentTime, currentValue]); + } + + if (max === -Number.MAX_VALUE) { + max = null; + } + + if (min === Number.MAX_VALUE) { + min = null; + } + + if (result.length && !allIsNull) { + avg = total / nonNulls; + current = result[result.length - 1][1]; + if (current === null && result.length > 1) { + current = result[result.length - 2][1]; + } + } + + if (max !== null && min !== null) { + range = max - min; + } + + if (current !== null && first !== null) { + diff = current - first; + } + + const count = result.length; + + return { + data: result, + label: label, + color: colors[colorIndex], + stats: { + total, + min, + max, + current, + logmin, + avg, + diff, + delta, + timeStep, + range, + count, + first, + allIsZero, + allIsNull, + }, + }; + }); + + return vmSeries; +} diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index f583e481490..e4c7a9c59e1 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -1,4 +1,7 @@ -// vendor +// DEPENDENCIES +@import '../../node_modules/react-table/react-table.css'; + +// VENDOR @import '../vendor/css/timepicker.css'; @import '../vendor/css/spectrum.css'; @import '../vendor/css/rc-cascader.scss'; @@ -94,6 +97,7 @@ @import 'components/form_select_box'; @import 'components/user-picker'; @import 'components/description-picker'; +@import 'components/viz_editor'; @import 'components/delete_button'; @import 'components/add_data_source.scss'; @import 'components/page_loader'; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index f1908ca8786..da1f140d252 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -20,7 +20,6 @@ } // Disable grid interaction indicators in fullscreen panels - .panel-header:hover { background-color: inherit; } @@ -32,6 +31,11 @@ .react-resizable-handle { display: none; } + + // the react-grid has a height transition + .react-grid-layout { + transition-property: none; + } } @include media-breakpoint-down(sm) { diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index 5bfff31a108..263b181262e 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -85,10 +85,6 @@ height: calc(100% - 15px); } -.add-panel__item-icon { - padding: 2px; -} - .add-panel__searchbar { width: 100%; margin-bottom: 10px; diff --git a/public/sass/components/_scrollbar.scss b/public/sass/components/_scrollbar.scss index adb9e0c54c0..00bd5f7c94c 100644 --- a/public/sass/components/_scrollbar.scss +++ b/public/sass/components/_scrollbar.scss @@ -307,6 +307,7 @@ .view { display: flex; flex-grow: 1; + flex-direction: column; } .track-vertical { @@ -337,3 +338,7 @@ border-radius: 6px; } } + +.scroll-margin-helper { + margin-right: 12px; +} diff --git a/public/sass/components/_slate_editor.scss b/public/sass/components/_slate_editor.scss index a8291aa1a87..b70990d4618 100644 --- a/public/sass/components/_slate_editor.scss +++ b/public/sass/components/_slate_editor.scss @@ -2,6 +2,7 @@ font-size: $font-size-root; font-family: $font-family-monospace; height: auto; + word-break: break-word; } .slate-query-field-wrapper { diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index bf95d453504..87b43a31142 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -1,19 +1,15 @@ .tabbed-view { - padding: $spacer*3; - margin-bottom: $dashboard-padding; + display: flex; + flex-direction: column; + height: 100%; - &.tabbed-view--panel-edit { - padding: 0; - - .tabbed-view-header { - padding: 0px 25px; - background: none; - } + &.tabbed-view--new { + padding: 25px 0 0 0; + height: 100%; } } .tabbed-view-header { - background: $page-header-bg; box-shadow: $page-header-shadow; border-bottom: 1px solid $page-header-border-color; @include clearfix(); @@ -48,7 +44,10 @@ } .tabbed-view-body { - padding: $spacer*2 $spacer; + padding: $spacer*2 $spacer $spacer $spacer; + display: flex; + flex-direction: column; + flex: 1; &--small { min-height: 0px; diff --git a/public/sass/components/_viz_editor.scss b/public/sass/components/_viz_editor.scss new file mode 100644 index 00000000000..048e513cfbb --- /dev/null +++ b/public/sass/components/_viz_editor.scss @@ -0,0 +1,81 @@ +.viz-editor { + display: flex; + height: 100%; +} + +.viz-editor-col1 { + width: 210px; + height: 100%; + margin-right: 40px; +} + +.viz-editor-col2 { + flex-grow: 1; +} + +.viz-picker { + display: flex; + flex-direction: column; + height: 100%; +} + +.viz-picker__search { + flex-grow: 0; +} + +.viz-picker__items { + flex-grow: 1; + height: calc(100% - 50px); +} + +.viz-picker__item { + background: $card-background; + box-shadow: $card-shadow; + + border-radius: 3px; + padding: $spacer; + width: 100%; + height: 60px; + text-align: center; + margin-bottom: 6px; + cursor: pointer; + display: flex; + flex-shrink: 0; + border: 1px solid transparent; + @include left-brand-border; + + &:hover { + background: $card-background-hover; + } + + &--selected { + // border: 1px solid $orange; + @include left-brand-border-gradient(); + + .viz-picker__item-name { + color: $text-color; + } + + .viz-picker__item-img { + filter: saturate(100%); + } + } +} + +.viz-picker__item-name { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + font-size: $font-size-h5; + display: flex; + flex-direction: column; + align-self: center; + padding-left: $spacer; + font-size: $font-size-md; + color: $text-muted; +} + +.viz-picker__item-img { + height: 100%; + filter: saturate(30%); +} diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 6225f840973..d9ab29cc91c 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -1,7 +1,12 @@ .dashboard-container { padding: $dashboard-padding $dashboard-padding 0 $dashboard-padding; width: 100%; - min-height: 100%; + height: 100%; + box-sizing: border-box; + + &--has-submenu { + height: calc(100% - 50px); + } } .template-variable { @@ -29,16 +34,43 @@ div.flot-text { height: 100%; } +.panel-editor-container { + display: flex; + flex-direction: column; + height: 100%; +} + +.panel-editor-container__panel { + height: 35%; +} + +.panel-editor-container__editor { + height: 65%; +} + .panel-container { background-color: $panel-bg; border: $panel-border; position: relative; border-radius: 3px; + height: 100%; &.panel-transparent { background-color: transparent; border: none; } + + &:hover { + .panel-menu-toggle { + visibility: visible; + transition: opacity 0.1s ease-in 0.2s; + opacity: 1; + } + } + + &--is-editing { + height: auto; + } } .panel-content { @@ -199,14 +231,6 @@ div.flot-text { } } -.panel-hover-highlight { - .panel-menu-toggle { - visibility: visible; - transition: opacity 0.1s ease-in 0.2s; - opacity: 1; - } -} - .panel-time-info { font-weight: bold; float: right; @@ -233,5 +257,5 @@ div.flot-text { } .panel-full-edit { - margin: $dashboard-padding (-$dashboard-padding) 0 (-$dashboard-padding); + padding-top: $dashboard-padding; } diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index f8bd1108fb1..c1198ce06be 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -126,7 +126,7 @@ } .query-row-tools { - width: 6rem; + white-space: nowrap; } .query-row-field { @@ -186,3 +186,60 @@ margin: 0.25em 0.5em 0.5em; } } + +// ReactTable basic overrides (does not include pivot/groups/filters) +// When integrating ReactTable as new panel plugin, move to _panel_table.scss + +.ReactTable { + border: none; + // Allow some space for the no-data text + min-height: 120px; +} + +.ReactTable .rt-thead.-header { + box-shadow: none; + background: $list-item-bg; + border-top: 2px solid $body-bg; + border-bottom: 2px solid $body-bg; + height: 2em; +} +.ReactTable .rt-thead.-header .rt-th { + text-align: left; + color: $blue; + font-weight: 500; +} +.ReactTable .rt-thead .rt-td, +.ReactTable .rt-thead .rt-th { + padding: 0.45em 0 0.45em 1.1em; + border-right: none; + box-shadow: none; +} +.ReactTable .rt-tbody .rt-td { + padding: 0.45em 0 0.45em 1.1em; + border-bottom: 2px solid $body-bg; + border-right: 2px solid $body-bg; +} +.ReactTable .rt-tbody .rt-td:last-child { + border-right: none; +} +.ReactTable .-pagination .-btn { + color: $blue; + background: $list-item-bg; +} +.ReactTable .-pagination input, +.ReactTable .-pagination select { + color: $input-color; + background-color: $input-bg; +} +.ReactTable .-loading { + background: $input-bg; +} +.ReactTable .-loading.-active { + opacity: 0.8; +} +.ReactTable .-loading > div { + color: $input-color; +} +.ReactTable .rt-tr .rt-td:last-child { + text-align: right; +} diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 0aaab2ce4a6..64e51ca6259 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -4,6 +4,10 @@ # This script is executed from within the container. # +set -e + +EXTRA_OPTS="$@" + CCARMV7=arm-linux-gnueabihf-gcc CCARM64=aarch64-linux-gnu-gcc CCOSX64=/tmp/osxcross/target/bin/o64-clang @@ -18,15 +22,20 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false" + OPT="-includeBuildNumber=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM}" + OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" fi +echo "Build arguments: $OPT" + go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build -go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build + +# MacOS build is broken atm. See Issue #13763 +#go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build + go run build.go -goos windows -cc ${CCWIN64} ${OPT} build CC=${CCX64} go run build.go ${OPT} build diff --git a/scripts/build/build.sh b/scripts/build/build.sh index d4c1c788b30..2cf9f5a8a21 100755 --- a/scripts/build/build.sh +++ b/scripts/build/build.sh @@ -4,6 +4,10 @@ # This script is executed from within the container. # +set -e + +EXTRA_OPTS="$@" + CCX64=/tmp/x86_64-centos6-linux-gnu/bin/x86_64-centos6-linux-gnu-gcc GOPATH=/go @@ -14,12 +18,14 @@ echo "current dir: $(pwd)" if [ "$CIRCLE_TAG" != "" ]; then echo "Building releases from tag $CIRCLE_TAG" - OPT="-includeBuildNumber=false" + OPT="-includeBuildNumber=false ${EXTRA_OPTS}" else echo "Building incremental build for $CIRCLE_BRANCH" - OPT="-buildNumber=${CIRCLE_BUILD_NUM}" + OPT="-buildNumber=${CIRCLE_BUILD_NUM} ${EXTRA_OPTS}" fi +echo "Build arguments: $OPT" + CC=${CCX64} go run build.go ${OPT} build yarn install --pure-lockfile --no-progress diff --git a/scripts/build/build_enterprise.sh b/scripts/build/build_enterprise.sh deleted file mode 100755 index cda3952c36a..00000000000 --- a/scripts/build/build_enterprise.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash - -# -# This script is executed from within the container. -# - -echo "building enterprise version" - -GOPATH=/go -REPO_PATH=$GOPATH/src/github.com/grafana/grafana - - -cd /go/src/github.com/grafana/grafana -echo "current dir: $(pwd)" - -cd .. -git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 10 -cd grafana-enterprise -#git checkout 7fbae9c1be3467c4a39cf6ad85278a6896ceb49f -./build.sh - -cd ../grafana - -function exit_if_fail { - command=$@ - echo "Executing '$command'" - eval $command - rc=$? - if [ $rc -ne 0 ]; then - echo "'$command' returned $rc." - exit $rc - fi -} - -exit_if_fail go test ./pkg/extensions/... - - -if [ "$CIRCLE_TAG" != "" ]; then - echo "Building a release from tag $ls" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false build -else - echo "Building incremental build for $CIRCLE_BRANCH" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true build -fi - -yarn install --pure-lockfile --no-progress - -source /etc/profile.d/rvm.sh - -echo "current dir: $(pwd)" - -if [ "$CIRCLE_TAG" != "" ]; then - echo "Packaging a release from tag $CIRCLE_TAG" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true -includeBuildNumber=false package latest -else - echo "Packaging incremental build for $CIRCLE_BRANCH" - go run build.go -buildNumber=${CIRCLE_BUILD_NUM} -enterprise=true package latest -fi diff --git a/scripts/build/prepare-enterprise.sh b/scripts/build/prepare-enterprise.sh new file mode 100755 index 00000000000..0e1c3da2dbd --- /dev/null +++ b/scripts/build/prepare-enterprise.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +cd .. +git clone -b master --single-branch git@github.com:grafana/grafana-enterprise.git --depth 1 +cd grafana-enterprise +./build.sh diff --git a/tools/phantomjs/render.js b/tools/phantomjs/render.js index 900660b7ca0..d0fb0234417 100644 --- a/tools/phantomjs/render.js +++ b/tools/phantomjs/render.js @@ -50,19 +50,22 @@ function checkIsReady() { var panelsRendered = page.evaluate(function() { - var panelCount = document.querySelectorAll('.panel').length; + var panelCount = document.querySelectorAll('plugin-component').length; return window.panelsRendered >= panelCount; }); if (panelsRendered || totalWaitMs > timeoutMs) { var bb = page.evaluate(function () { - return document.getElementsByClassName("main-view")[0].getBoundingClientRect(); + var container = document.getElementsByClassName("dashboard-container") + if (container.length == 0) { + container = document.getElementsByClassName("panel-container") + } + return container[0].getBoundingClientRect(); }); - - page.clipRect = { - top: bb.top, - left: bb.left, - width: bb.width, + + // reset viewport to render full page + page.viewportSize = { + width: bb.width, height: bb.height }; diff --git a/yarn.lock b/yarn.lock index 7bfefef280e..5c5156b9970 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11378,6 +11378,13 @@ react-sizeme@^2.3.6: lodash.throttle "^4.1.1" shallowequal "^1.0.2" +react-table@^6.8.6: + version "6.8.6" + resolved "https://registry.yarnpkg.com/react-table/-/react-table-6.8.6.tgz#a0ad8b4839319052d5befc012603fb161e52ede3" + integrity sha1-oK2LSDkxkFLVvvwBJgP7Fh5S7eM= + dependencies: + classnames "^2.2.5" + react-test-renderer@^16.0.0-0, react-test-renderer@^16.5.0: version "16.5.2" resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.5.2.tgz#92e9d2c6f763b9821b2e0b22f994ee675068b5ae" @@ -13540,7 +13547,7 @@ tryor@~0.1.2: resolved "https://registry.yarnpkg.com/tryor/-/tryor-0.1.2.tgz#8145e4ca7caff40acde3ccf946e8b8bb75b4172b" integrity sha1-gUXkynyv9ArN48z5Rui4u3W0Fys= -ts-jest@^23.1.4: +ts-jest@^23.10.4: version "23.10.4" resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.4.tgz#a7a953f55c9165bcaa90ff91014a178e87fe0df8" integrity sha512-oV/wBwGUS7olSk/9yWMiSIJWbz5xO4zhftnY3gwv6s4SMg6WHF1m8XZNBvQOKQRiTAexZ9754Z13dxBq3Zgssw==