diff --git a/.bra.toml b/.bra.toml index aa7a1680adc..5be42ceebbf 100644 --- a/.bra.toml +++ b/.bra.toml @@ -1,7 +1,7 @@ [run] init_cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] watch_all = true follow_symlinks = true @@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"] build_delay = 1500 cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] diff --git a/.circleci/config.yml b/.circleci/config.yml index 28400fc6bd6..dc30554fc5c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,12 +85,13 @@ jobs: - run: 'go get -u github.com/tsenart/deadcode' - run: 'go get -u github.com/jgautheron/goconst/cmd/goconst' - run: 'go get -u github.com/gordonklaus/ineffassign' + - run: 'go get -u honnef.co/go/tools/cmd/megacheck' - run: 'go get -u github.com/opennota/check/cmd/structcheck' - run: 'go get -u github.com/mdempsky/unconvert' - run: 'go get -u github.com/opennota/check/cmd/varcheck' - run: name: run linters - command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' + command: 'gometalinter --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=goconst --enable=gofmt --enable=ineffassign --enable=megacheck --enable=structcheck --enable=unconvert --enable=varcheck ./...' - run: name: run go vet command: 'go vet ./pkg/...' @@ -126,7 +127,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -174,7 +175,7 @@ jobs: build: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -240,7 +241,7 @@ jobs: build-enterprise: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -272,7 +273,7 @@ jobs: build-all-enterprise: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -319,39 +320,55 @@ jobs: deploy-enterprise-master: docker: - - image: circleci/python:2.7-stretch + - image: grafana/grafana-ci-deploy:1.0.0 steps: - attach_workspace: at: . - run: - name: install awscli - command: 'sudo pip install awscli' + name: gcp credentials + command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json' + - run: + name: sign in to gcp + command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' - run: name: deploy to s3 command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/master' + - run: + name: deploy to gcp + command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/master' + - run: + name: Deploy to grafana.com + command: 'cd enterprise-dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -enterprise -from-local' + deploy-enterprise-release: docker: - - image: circleci/python:2.7-stretch + - image: grafana/grafana-ci-deploy:1.0.0 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' + - attach_workspace: + at: . + - run: + name: gcp credentials + command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json' + - run: + name: sign in to gcp + command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' + - run: + name: deploy to s3 + command: 'aws s3 sync ./enterprise-dist s3://$ENTERPRISE_BUCKET_NAME/release' + - run: + name: deploy to gcp + command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release' + - run: + name: Deploy to Grafana.com + command: './scripts/build/publish.sh --enterprise' deploy-master: docker: - - image: circleci/python:2.7-stretch + - image: grafana/grafana-ci-deploy:1.0.0 steps: - attach_workspace: at: . - - run: - name: install awscli - command: 'sudo pip install awscli' - run: name: deploy to s3 command: | @@ -361,6 +378,15 @@ jobs: - run: name: Trigger Windows build command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master' + - run: + name: gcp credentials + command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json' + - run: + name: sign in to gcp + command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' + - run: + name: deploy to gcp + command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/master' - run: name: Publish to Grafana.com command: | @@ -369,16 +395,22 @@ jobs: deploy-release: docker: - - image: circleci/python:2.7-stretch + - image: grafana/grafana-ci-deploy:1.0.0 steps: - attach_workspace: at: . - - run: - name: install awscli - command: 'sudo pip install awscli' - run: name: deploy to s3 command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release' + - run: + name: gcp credentials + command: 'echo ${GCP_GRAFANA_UPLOAD_KEY} > /tmp/gcpkey.json' + - run: + name: sign in to gcp + command: '/opt/google-cloud-sdk/bin/gcloud auth activate-service-account --key-file=/tmp/gcpkey.json' + - run: + name: deploy to gcp + command: '/opt/google-cloud-sdk/bin/gsutil cp ./dist/* gs://$GCP_BUCKET_NAME/oss/release' - run: name: Deploy to Grafana.com command: './scripts/build/publish.sh' @@ -478,6 +510,7 @@ workflows: - grafana-docker-release: requires: - build-all + - build-all-enterprise - test-backend - test-frontend - codespell diff --git a/.gitignore b/.gitignore index 21083741e14..05ae4907e89 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ awsconfig /dist /public/build /public/views/index.html +/public/views/error.html /emails/dist /public_gen /public/vendor/npm diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b14126e5c1..571989ea506 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,27 +1,68 @@ # 5.4.0 (unreleased) +* **Cloudwatch**: Fix invalid time range causes segmentation fault [#14150](https://github.com/grafana/grafana/issues/14150) + +# 5.4.0-beta1 (2018-11-20) + ### New Features +* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202) * **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**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm) * **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) +* **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) * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) +* **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550) +* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930) +* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller) +* **Stackdriver**: Template query editor [#13561](https://github.com/grafana/grafana/issues/13561) ### Minor +* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043) * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy) +* **Cloudwatch**: CloudHSM metrics and dimensions [#14129](https://github.com/grafana/grafana/pull/14129), thx [@daktari](https://github.com/daktari) +* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda) +* **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) +* **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367) +* **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977) +* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei) +* **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme) +* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia) * **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) +* **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945) +* **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov) +* **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) +* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio) +* **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) -* **DingDing**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) +* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) +* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat) +* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry) +* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425) +* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru) +* **HTTP API**: Support retrieving teams by user [#14120](https://github.com/grafana/grafana/pull/14120), thx [@supercharlesliu](https://github.com/supercharlesliu) +* **Metrics**: Add basic authentication to metrics endpoint [#13577](https://github.com/grafana/grafana/issues/13577), thx [@bobmshannon](https://github.com/bobmshannon) ### Breaking changes -* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited) +* Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited). -# 5.3.3 (unreleased) +# 5.3.4 (2018-11-13) +* **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) * **MySQL**: Fix `$__timeFilter()` should respect local time zone [#13769](https://github.com/grafana/grafana/issues/13769) +* **Dashboard**: Fix datasource selection in panel by enter key [#13932](https://github.com/grafana/grafana/issues/13932) +* **Graph**: Fix table legend height when positioned below graph and using Internet Explorer 11 [#13903](https://github.com/grafana/grafana/issues/13903) +* **Dataproxy**: Drop origin and referer http headers [#13328](https://github.com/grafana/grafana/issues/13328) [#13949](https://github.com/grafana/grafana/issues/13949), thx [@roidelapluie](https://github.com/roidelapluie) + +# 5.3.3 (2018-11-13) + +### File Exfiltration vulnerability Security fix + +See [security announcement](https://community.grafana.com/t/grafana-5-3-3-and-4-6-5-security-update/11961) for details. # 5.3.2 (2018-10-24) diff --git a/Gopkg.lock b/Gopkg.lock index 4286add847d..1e06d4cbc5d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -2,30 +2,39 @@ [[projects]] + digest = "1:f8ad8a53fa865a70efbe215b0ca34735523f50ea39e0efde319ab6fc80089b44" name = "cloud.google.com/go" packages = ["compute/metadata"] + pruneopts = "NUT" revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811" version = "v0.22.0" [[projects]] + digest = "1:167b6f65a6656de568092189ae791253939f076df60231fdd64588ac703892a1" name = "github.com/BurntSushi/toml" packages = ["."] + pruneopts = "NUT" revision = "b26d9c308763d68093482582cea63d69be07a0f0" version = "v0.3.0" [[projects]] branch = "master" + digest = "1:7d23e6e1889b8bb4bbb37a564708fdab4497ce232c3a99d66406c975b642a6ff" name = "github.com/Unknwon/com" packages = ["."] + pruneopts = "NUT" revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520" [[projects]] branch = "master" + digest = "1:1610787cd9726e29d8fecc2a80e43e4fced008a1f560fec6688fc4d946f17835" name = "github.com/VividCortex/mysqlerr" packages = ["."] + pruneopts = "NUT" revision = "6c6b55f8796f578c870b7e19bafb16103bc40095" [[projects]] + digest = "1:58294d68772aab5a8941b7d5d228eff7cccf63f895e914bc9bc38fda80471ea5" name = "github.com/aws/aws-sdk-go" packages = [ "aws", @@ -61,405 +70,523 @@ "service/ec2", "service/ec2/ec2iface", "service/s3", - "service/sts" + "service/sts", ] + pruneopts = "NUT" revision = "fde4ded7becdeae4d26bf1212916aabba79349b4" version = "v1.14.12" [[projects]] branch = "master" + digest = "1:79cad073c7be02632d3fa52f62486848b089f560db1e94536de83a408c0f4726" name = "github.com/benbjohnson/clock" packages = ["."] + pruneopts = "NUT" revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19" [[projects]] branch = "master" + digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd" name = "github.com/beorn7/perks" packages = ["quantile"] + pruneopts = "NUT" revision = "3a771d992973f24aa725d07868b467d1ddfceafb" [[projects]] branch = "master" + digest = "1:433a2ff0ef4e2f8634614aab3174783c5ff80120b487712db96cc3712f409583" name = "github.com/bmizerany/assert" packages = ["."] + pruneopts = "NUT" revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28" [[projects]] branch = "master" + digest = "1:d8f9145c361920507a4f85ffb7f70b96beaedacba2ce8c00aa663adb08689d3e" name = "github.com/bradfitz/gomemcache" packages = ["memcache"] + pruneopts = "NUT" revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b" [[projects]] branch = "master" + digest = "1:8ecb89af7dfe3ac401bdb0c9390b134ef96a97e85f732d2b0604fb7b3977839f" name = "github.com/codahale/hdrhistogram" packages = ["."] + pruneopts = "NUT" revision = "3a0bb77429bd3a61596f5e8a3172445844342120" [[projects]] + digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58" name = "github.com/codegangsta/cli" packages = ["."] + pruneopts = "NUT" revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1" version = "v1.20.0" [[projects]] + digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39" name = "github.com/davecgh/go-spew" packages = ["spew"] + pruneopts = "NUT" revision = "346938d642f2ec3594ed81d874461961cd0faa76" version = "v1.1.0" [[projects]] + digest = "1:1b318d2dd6cea8a1a8d8ec70348852303bd3e491df74e8bca6e32eb5a4d06970" name = "github.com/denisenkom/go-mssqldb" packages = [ ".", - "internal/cp" + "internal/cp", ] + pruneopts = "NUT" revision = "270bc3860bb94dd3a3ffd047377d746c5e276726" [[projects]] branch = "master" + digest = "1:2da5f11ad66ff01a27a5c3dba4620b7eee2327be75b32c9ee9f87c9a8001ecbf" name = "github.com/facebookgo/inject" packages = ["."] + pruneopts = "NUT" revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b" [[projects]] branch = "master" + digest = "1:1108df7f658c90db041e0d6174d55be689aaeb0585913b9c3c7aab51a3a6b2b1" name = "github.com/facebookgo/structtag" packages = ["."] + pruneopts = "NUT" revision = "217e25fb96916cc60332e399c9aa63f5c422ceed" [[projects]] + digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45" name = "github.com/fatih/color" packages = ["."] + pruneopts = "NUT" revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4" version = "v1.7.0" [[projects]] + digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474" name = "github.com/go-ini/ini" packages = ["."] + pruneopts = "NUT" revision = "6529cf7c58879c08d927016dde4477f18a0634cb" version = "v1.36.0" [[projects]] + digest = "1:7e1c00b9959544fa1ccca7cf0407a5b29ac6d5201059c4fac6f599cb99bfd24d" name = "github.com/go-ldap/ldap" packages = ["."] + pruneopts = "NUT" revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9" version = "v2.5.1" [[projects]] branch = "master" + digest = "1:682a0aca743a1a4a36697f3d7f86c0ed403c4e3a780db9935f633242855eac9c" name = "github.com/go-macaron/binding" packages = ["."] + pruneopts = "NUT" revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183" [[projects]] branch = "master" + digest = "1:6326b27f8e0c8e135c8674ddbc619fae879664ac832e8e6fa6a23ce0d279ed4d" name = "github.com/go-macaron/gzip" packages = ["."] + pruneopts = "NUT" revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854" [[projects]] branch = "master" + digest = "1:fb8711b648d1ff03104fc1d9593a13cb1d5120be7ba2b01641c14ccae286a9e3" name = "github.com/go-macaron/inject" packages = ["."] + pruneopts = "NUT" revision = "d8a0b8677191f4380287cfebd08e462217bac7ad" [[projects]] branch = "master" + digest = "1:21577aafe885f088e8086a3415f154c63c0b7ce956a6994df2ac5776bc01b7e3" name = "github.com/go-macaron/session" packages = [ ".", "memcache", "postgres", - "redis" + "redis", ] - revision = "b8e286a0dba8f4999042d6b258daf51b31d08938" + pruneopts = "NUT" + revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e" [[projects]] + digest = "1:fddd4bada6100d6fc49a9f32f18ba5718db45a58e4b00aa6377e1cfbf06af34f" name = "github.com/go-sql-driver/mysql" packages = ["."] + pruneopts = "NUT" revision = "2cc627ac8defc45d65066ae98f898166f580f9a4" [[projects]] + digest = "1:a1efdbc2762667c8a41cbf02b19a0549c846bf2c1d08cad4f445e3344089f1f0" name = "github.com/go-stack/stack" packages = ["."] + pruneopts = "NUT" revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc" version = "v1.7.0" [[projects]] + digest = "1:b9d4f09cdaaa9e7dca5ed0b501ca5519eb2168dd01fc5d174c54edfe42a7d5ed" name = "github.com/go-xorm/builder" packages = ["."] + pruneopts = "NUT" revision = "bad0a612f0d6277b953910822ab5dfb30dd18237" version = "v0.2.0" [[projects]] + digest = "1:30fb106b0cd6d64ea6fccded579c8f7d788460092f885fcc8f3edd02fc2085a4" name = "github.com/go-xorm/core" packages = ["."] + pruneopts = "NUT" revision = "da1adaf7a28ca792961721a34e6e04945200c890" version = "v0.5.7" [[projects]] + digest = "1:007d1354e4f44e6a393337e7623bcf911dfe75d6ef30fb767a6a0b65d302f5ed" name = "github.com/go-xorm/xorm" packages = ["."] + pruneopts = "NUT" revision = "1933dd69e294c0a26c0266637067f24dbb25770c" version = "v0.6.4" [[projects]] branch = "master" + digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0" name = "github.com/golang/protobuf" packages = [ "proto", "ptypes", "ptypes/any", "ptypes/duration", - "ptypes/timestamp" + "ptypes/timestamp", ] + pruneopts = "NUT" revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0" [[projects]] branch = "master" + digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795" name = "github.com/gopherjs/gopherjs" packages = ["js"] + pruneopts = "NUT" revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5" [[projects]] + digest = "1:3b708ebf63bfa9ba3313bedb8526bc0bb284e51474e65e958481476a9d4a12aa" name = "github.com/gorilla/websocket" packages = ["."] + pruneopts = "NUT" revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" version = "v1.2.0" [[projects]] + digest = "1:4e771d1c6e15ca4516ad971c34205c822b5cff2747179679d7b321e4e1bfe431" name = "github.com/gosimple/slug" packages = ["."] + pruneopts = "NUT" revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6" version = "v1.1.1" [[projects]] branch = "master" + digest = "1:08e53c69cd267ef7d71eeae5d953153d0d2bc1b8e0b498731fe9acaead7001b6" name = "github.com/grafana/grafana-plugin-model" packages = [ "go/datasource", - "go/renderer" + "go/renderer", ] + pruneopts = "NUT" revision = "84176c64269d8060f99e750ee8aba6f062753336" [[projects]] branch = "master" + digest = "1:58ba5285227b0f635652cd4aa82c4cfd00b590191eadd823462f0c9f64e3ae07" name = "github.com/hashicorp/go-hclog" packages = ["."] + pruneopts = "NUT" revision = "69ff559dc25f3b435631604f573a5fa1efdb6433" [[projects]] + digest = "1:532090ffc3b05a7e4c0229dd2698d79149f2e0683df993224a8b202f607fb605" name = "github.com/hashicorp/go-plugin" packages = ["."] + pruneopts = "NUT" revision = "e8d22c780116115ae5624720c9af0c97afe4f551" [[projects]] branch = "master" + digest = "1:8925116d1edcd85fc0c014e1aa69ce12892489b48ee633a605c46d893b8c151f" name = "github.com/hashicorp/go-version" packages = ["."] + pruneopts = "NUT" revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7" [[projects]] branch = "master" + digest = "1:8deb0c5545c824dfeb0ac77ab8eb67a3d541eab76df5c85ce93064ef02d44cd0" name = "github.com/hashicorp/yamux" packages = ["."] + pruneopts = "NUT" revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558" [[projects]] + digest = "1:efbe016b6d198cf44f1db0ed2fbdf1b36ebf1f6956cc9b76d6affa96f022d368" name = "github.com/inconshreveable/log15" packages = ["."] + pruneopts = "NUT" revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3" version = "v2.13" [[projects]] + digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc" name = "github.com/jmespath/go-jmespath" packages = ["."] + pruneopts = "NUT" revision = "0b12d6b5" [[projects]] + digest = "1:6ddab442e52381bab82fb6c07ef3f4b565ff7ec4b8fae96d8dd4b8573a460597" name = "github.com/jtolds/gls" packages = ["."] + pruneopts = "NUT" revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064" version = "v4.2.1" [[projects]] + digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3" name = "github.com/klauspost/compress" packages = [ "flate", - "gzip" + "gzip", ] + pruneopts = "NUT" revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf" version = "v1.2.1" [[projects]] + digest = "1:5e55a8699c9ff7aba1e4c8952aeda209685d88d4cb63a8766c338e333b8e65d6" name = "github.com/klauspost/cpuid" packages = ["."] + pruneopts = "NUT" revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da" version = "v1.1" [[projects]] + digest = "1:b95da1293525625ef6f07be79d537b9bf2ecd7901efcf9a92193edafbd55b9ef" name = "github.com/klauspost/crc32" packages = ["."] + pruneopts = "NUT" revision = "cb6bfca970f6908083f26f39a79009d608efd5cd" version = "v1.1" [[projects]] + digest = "1:7b21c7fc5551b46d1308b4ffa9e9e49b66c7a8b0ba88c0130474b0e7a20d859f" name = "github.com/kr/pretty" packages = ["."] + pruneopts = "NUT" revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712" version = "v0.1.0" [[projects]] + digest = "1:c3a7836b5904db0f8b609595b619916a6831cb35b8b714aec39f96d00c6155d8" name = "github.com/kr/text" packages = ["."] + pruneopts = "NUT" revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f" version = "v0.1.0" [[projects]] branch = "master" + digest = "1:7a1e592f0349d56fac8ce47f28469e4e7f4ce637cb26f40c88da9dff25db1c98" name = "github.com/lib/pq" packages = [ ".", - "oid" + "oid", ] + pruneopts = "NUT" revision = "d34b9ff171c21ad295489235aec8b6626023cd04" [[projects]] + digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061" name = "github.com/mattn/go-colorable" packages = ["."] + pruneopts = "NUT" revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072" version = "v0.0.9" [[projects]] + digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287" name = "github.com/mattn/go-isatty" packages = ["."] + pruneopts = "NUT" revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39" version = "v0.0.3" [[projects]] + digest = "1:536979f1c56397dbf91c2785159b37dec37e35d3bffa3cd1cfe66d25f51f8088" name = "github.com/mattn/go-sqlite3" packages = ["."] + pruneopts = "NUT" revision = "323a32be5a2421b8c7087225079c6c900ec397cd" version = "v1.7.0" [[projects]] + digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6" name = "github.com/matttproud/golang_protobuf_extensions" packages = ["pbutil"] + pruneopts = "NUT" revision = "3247c84500bff8d9fb6d579d800f20b3e091582c" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048" name = "github.com/mitchellh/go-testing-interface" packages = ["."] + pruneopts = "NUT" revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28" [[projects]] + digest = "1:3b517122f3aad1ecce45a630ea912b3092b4729f25532a911d0cb2935a1f9352" name = "github.com/oklog/run" packages = ["."] + pruneopts = "NUT" revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39" version = "v1.0.0" [[projects]] + digest = "1:7da29c22bcc5c2ffb308324377dc00b5084650348c2799e573ed226d8cc9faf0" name = "github.com/opentracing/opentracing-go" packages = [ ".", "ext", - "log" + "log", ] + pruneopts = "NUT" revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38" version = "v1.0.2" [[projects]] + digest = "1:748946761cf99c8b73cef5a3c0ee3e040859dd713a20cece0d0e0dc04e6ceca7" name = "github.com/patrickmn/go-cache" packages = ["."] + pruneopts = "NUT" revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" version = "v2.1.0" [[projects]] + digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121" name = "github.com/pkg/errors" packages = ["."] + pruneopts = "NUT" revision = "645ef00459ed84a119197bfb8d8205042c6df63d" version = "v0.8.0" [[projects]] + digest = "1:4759bed95e3a52febc18c071db28790a5c6e9e106ee201a37add6f6a056f8f9c" name = "github.com/prometheus/client_golang" packages = [ "api", "api/prometheus/v1", "prometheus", - "prometheus/promhttp" + "prometheus/promhttp", ] + pruneopts = "NUT" revision = "967789050ba94deca04a5e84cce8ad472ce313c1" version = "v0.9.0-pre1" [[projects]] branch = "master" + digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b" name = "github.com/prometheus/client_model" packages = ["go"] + pruneopts = "NUT" revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" [[projects]] branch = "master" + digest = "1:768b555b86742de2f28beb37f1dedce9a75f91f871d75b5717c96399c1a78c08" name = "github.com/prometheus/common" packages = [ "expfmt", "internal/bitbucket.org/ww/goautoneg", - "model" + "model", ] + pruneopts = "NUT" revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c" [[projects]] branch = "master" + digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349" name = "github.com/prometheus/procfs" packages = [ ".", "internal/util", "nfs", - "xfs" + "xfs", ] + pruneopts = "NUT" revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e" [[projects]] branch = "master" + digest = "1:16e2136a67ec44aa2d1d6b0fd65394b3c4a8b2a1b6730c77967f7b7b06b179b2" name = "github.com/rainycape/unidecode" packages = ["."] + pruneopts = "NUT" revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c" [[projects]] + digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04" name = "github.com/sergi/go-diff" packages = ["diffmatchpatch"] + pruneopts = "NUT" revision = "1744e2970ca51c86172c8190fadad617561ed6e7" version = "v1.0.0" [[projects]] + digest = "1:1f0b284a6858827de4c27c66b49b2b25df3e16b031c2b57b7892273131e7dd2b" name = "github.com/smartystreets/assertions" packages = [ ".", "internal/go-render/render", - "internal/oglematchers" + "internal/oglematchers", ] + pruneopts = "NUT" revision = "7678a5452ebea5b7090a6b163f844c133f523da2" version = "1.8.3" [[projects]] + digest = "1:7efd0b2309cdd6468029fa30c808c50a820c9344df07e1a4bbdaf18f282907aa" name = "github.com/smartystreets/goconvey" packages = [ "convey", "convey/gotest", - "convey/reporting" + "convey/reporting", ] + pruneopts = "NUT" revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857" version = "1.6.3" [[projects]] branch = "master" + digest = "1:a66add8dd963bfc72649017c1b321198f596cb4958cb1a11ff91a1be8691020b" name = "github.com/teris-io/shortid" packages = ["."] + pruneopts = "NUT" revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c" [[projects]] + digest = "1:3d48c38e0eca8c66df62379c5ae7a83fb5cd839b94f241354c07ba077da7bc45" name = "github.com/uber/jaeger-client-go" packages = [ ".", @@ -477,45 +604,55 @@ "thrift-gen/jaeger", "thrift-gen/sampling", "thrift-gen/zipkincore", - "utils" + "utils", ] + pruneopts = "NUT" revision = "b043381d944715b469fd6b37addfd30145ca1758" version = "v2.14.0" [[projects]] + digest = "1:0f09db8429e19d57c8346ad76fbbc679341fa86073d3b8fb5ac919f0357d8f4c" name = "github.com/uber/jaeger-lib" packages = ["metrics"] + pruneopts = "NUT" revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5" version = "v1.5.0" [[projects]] + digest = "1:4c7d12ad3ef47bb03892a52e2609dc9a9cff93136ca9c7d31c00b79fcbc23c7b" name = "github.com/yudai/gojsondiff" packages = [ ".", - "formatter" + "formatter", ] + pruneopts = "NUT" revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6" version = "1.0.0" [[projects]] branch = "master" + digest = "1:e50cbf8eba568d59b71e08c22c2a77809ed4646ae06ef4abb32b3d3d3fdb1a77" name = "github.com/yudai/golcs" packages = ["."] + pruneopts = "NUT" revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68" [[projects]] branch = "master" + digest = "1:758f363e0dff33cf00b234be2efb12f919d79b42d5ae3909ff9eb69ef2c3cca5" name = "golang.org/x/crypto" packages = [ "ed25519", "ed25519/internal/edwards25519", "md4", - "pbkdf2" + "pbkdf2", ] + pruneopts = "NUT" revision = "1a580b3eff7814fc9b40602fd35256c63b50f491" [[projects]] branch = "master" + digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5" name = "golang.org/x/net" packages = [ "context", @@ -525,35 +662,43 @@ "http2/hpack", "idna", "internal/timeseries", - "trace" + "trace", ] + pruneopts = "NUT" revision = "2491c5de3490fced2f6cff376127c667efeed857" [[projects]] branch = "master" + digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44" name = "golang.org/x/oauth2" packages = [ ".", "google", "internal", "jws", - "jwt" + "jwt", ] + pruneopts = "NUT" revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269" [[projects]] branch = "master" + digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239" name = "golang.org/x/sync" packages = ["errgroup"] + pruneopts = "NUT" revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca" [[projects]] branch = "master" + digest = "1:ec21c5bf0572488865b93e30ffd9132afbf85bec0b20c2d6cbcf349cf2031ed5" name = "golang.org/x/sys" packages = ["unix"] + pruneopts = "NUT" revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b" [[projects]] + digest = "1:e7071ed636b5422cc51c0e3a6cebc229d6c9fffc528814b519a980641422d619" name = "golang.org/x/text" packages = [ "collate", @@ -569,12 +714,14 @@ "unicode/bidi", "unicode/cldr", "unicode/norm", - "unicode/rangetable" + "unicode/rangetable", ] + pruneopts = "NUT" revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" version = "v0.3.0" [[projects]] + digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b" name = "google.golang.org/appengine" packages = [ ".", @@ -587,18 +734,22 @@ "internal/modules", "internal/remote_api", "internal/urlfetch", - "urlfetch" + "urlfetch", ] + pruneopts = "NUT" revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a" version = "v1.0.0" [[projects]] branch = "master" + digest = "1:3c24554c312721e98fa6b76403e7100cf974eb46b1255ea7fc6471db9a9ce498" name = "google.golang.org/genproto" packages = ["googleapis/rpc/status"] + pruneopts = "NUT" revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff" [[projects]] + digest = "1:840b77b6eb539b830bb760b6e30b688ed2ff484bd83466fce2395835ed9367fe" name = "google.golang.org/grpc" packages = [ ".", @@ -625,72 +776,167 @@ "stats", "status", "tap", - "transport" + "transport", ] + pruneopts = "NUT" revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34" version = "v1.11.1" [[projects]] branch = "v3" + digest = "1:1244a9b3856f70d5ffb74bbfd780fc9d47f93f2049fa265c6fb602878f507bf8" name = "gopkg.in/alexcesaro/quotedprintable.v3" packages = ["."] + pruneopts = "NUT" revision = "2caba252f4dc53eaf6b553000885530023f54623" [[projects]] + digest = "1:aea6e9483c167cc6fdf1274c442558c5dda8fd3373372be04d98c79100868da1" name = "gopkg.in/asn1-ber.v1" packages = ["."] + pruneopts = "NUT" revision = "379148ca0225df7a432012b8df0355c2a2063ac0" version = "v1.2" [[projects]] + digest = "1:24bfc2e8bf971485cb5ba0f0e5b08a1b806cca5828134df76b32d1ea50f2ab49" name = "gopkg.in/bufio.v1" packages = ["."] + pruneopts = "NUT" revision = "567b2bfa514e796916c4747494d6ff5132a1dfce" version = "v1" [[projects]] + digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474" name = "gopkg.in/ini.v1" packages = ["."] + pruneopts = "NUT" revision = "6529cf7c58879c08d927016dde4477f18a0634cb" version = "v1.36.0" [[projects]] + digest = "1:3b0cf3a465fd07f76e5fc1a9d0783c662dac0de9fc73d713ebe162768fd87b5f" name = "gopkg.in/macaron.v1" packages = ["."] + pruneopts = "NUT" revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10" version = "v1.3.1" [[projects]] branch = "v2" + digest = "1:d52332f9e9f2c6343652e13aa3fd40cfd03353520c9a48d90f21215d3012d50f" name = "gopkg.in/mail.v2" packages = ["."] + pruneopts = "NUT" revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68" [[projects]] + digest = "1:00126f697efdcab42f07c89ac8bf0095fb2328aef6464e070055154088cea859" name = "gopkg.in/redis.v2" packages = ["."] + pruneopts = "NUT" revision = "e6179049628164864e6e84e973cfb56335748dea" version = "v2.3.2" [[projects]] + digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8" name = "gopkg.in/square/go-jose.v2" packages = [ ".", "cipher", - "json" + "json", ] + pruneopts = "NUT" revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" version = "v2.1.9" [[projects]] + branch = "v2" + digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082" name = "gopkg.in/yaml.v2" packages = ["."] + pruneopts = "NUT" revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "6f7f271afd27f78b7d8ebe27436fee72c9925fb82a978bdc57fde44e01f3ca51" + input-imports = [ + "github.com/BurntSushi/toml", + "github.com/Unknwon/com", + "github.com/VividCortex/mysqlerr", + "github.com/aws/aws-sdk-go/aws", + "github.com/aws/aws-sdk-go/aws/awserr", + "github.com/aws/aws-sdk-go/aws/awsutil", + "github.com/aws/aws-sdk-go/aws/credentials", + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds", + "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds", + "github.com/aws/aws-sdk-go/aws/defaults", + "github.com/aws/aws-sdk-go/aws/ec2metadata", + "github.com/aws/aws-sdk-go/aws/endpoints", + "github.com/aws/aws-sdk-go/aws/request", + "github.com/aws/aws-sdk-go/aws/session", + "github.com/aws/aws-sdk-go/service/cloudwatch", + "github.com/aws/aws-sdk-go/service/ec2", + "github.com/aws/aws-sdk-go/service/ec2/ec2iface", + "github.com/aws/aws-sdk-go/service/s3", + "github.com/aws/aws-sdk-go/service/sts", + "github.com/benbjohnson/clock", + "github.com/bmizerany/assert", + "github.com/codegangsta/cli", + "github.com/davecgh/go-spew/spew", + "github.com/denisenkom/go-mssqldb", + "github.com/facebookgo/inject", + "github.com/fatih/color", + "github.com/go-ldap/ldap", + "github.com/go-macaron/binding", + "github.com/go-macaron/gzip", + "github.com/go-macaron/session", + "github.com/go-macaron/session/memcache", + "github.com/go-macaron/session/postgres", + "github.com/go-macaron/session/redis", + "github.com/go-sql-driver/mysql", + "github.com/go-stack/stack", + "github.com/go-xorm/core", + "github.com/go-xorm/xorm", + "github.com/gorilla/websocket", + "github.com/gosimple/slug", + "github.com/grafana/grafana-plugin-model/go/datasource", + "github.com/grafana/grafana-plugin-model/go/renderer", + "github.com/hashicorp/go-hclog", + "github.com/hashicorp/go-plugin", + "github.com/hashicorp/go-version", + "github.com/inconshreveable/log15", + "github.com/lib/pq", + "github.com/mattn/go-isatty", + "github.com/mattn/go-sqlite3", + "github.com/opentracing/opentracing-go", + "github.com/opentracing/opentracing-go/ext", + "github.com/opentracing/opentracing-go/log", + "github.com/patrickmn/go-cache", + "github.com/pkg/errors", + "github.com/prometheus/client_golang/api", + "github.com/prometheus/client_golang/api/prometheus/v1", + "github.com/prometheus/client_golang/prometheus", + "github.com/prometheus/client_golang/prometheus/promhttp", + "github.com/prometheus/client_model/go", + "github.com/prometheus/common/expfmt", + "github.com/prometheus/common/model", + "github.com/smartystreets/goconvey/convey", + "github.com/teris-io/shortid", + "github.com/uber/jaeger-client-go/config", + "github.com/yudai/gojsondiff", + "github.com/yudai/gojsondiff/formatter", + "golang.org/x/net/context/ctxhttp", + "golang.org/x/oauth2", + "golang.org/x/oauth2/google", + "golang.org/x/oauth2/jwt", + "golang.org/x/sync/errgroup", + "gopkg.in/ini.v1", + "gopkg.in/macaron.v1", + "gopkg.in/mail.v2", + "gopkg.in/square/go-jose.v2", + "gopkg.in/yaml.v2", + ] solver-name = "gps-cdcl" solver-version = 1 diff --git a/Makefile b/Makefile index c9e51d897f3..6410714d4fc 100644 --- a/Makefile +++ b/Makefile @@ -5,8 +5,7 @@ all: deps build deps-go: go run build.go setup -deps-js: - yarn install --pure-lockfile --no-progress +deps-js: node_modules deps: deps-js @@ -26,7 +25,7 @@ build: build-go build-js build-docker-dev: @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already." - go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest + go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest cp dist/grafana-latest.linux-x64.tar.gz packaging/docker cd packaging/docker && docker build --tag grafana/grafana:dev . @@ -43,3 +42,10 @@ test: test-go test-js run: ./bin/grafana-server + +clean: + rm -rf node_modules + rm -rf public/build + +node_modules: package.json yarn.lock + yarn install --pure-lockfile --no-progress diff --git a/build.go b/build.go index 6fd55da25b6..9d5216de1d0 100644 --- a/build.go +++ b/build.go @@ -41,8 +41,8 @@ var ( race bool phjsToRelease string workingDir string - includeBuildNumber bool = true - buildNumber int = 0 + includeBuildId bool = true + buildId string = "0" binaries []string = []string{"grafana-server", "grafana-cli"} isDev bool = false enterprise bool = false @@ -54,6 +54,8 @@ func main() { ensureGoPath() + var buildIdRaw string + flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH") flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS") flag.StringVar(&gocc, "cc", "", "CC") @@ -61,12 +63,14 @@ func main() { flag.StringVar(&pkgArch, "pkg-arch", "", "PKG ARCH") flag.StringVar(&phjsToRelease, "phjs", "", "PhantomJS binary") flag.BoolVar(&race, "race", race, "Use race detector") - flag.BoolVar(&includeBuildNumber, "includeBuildNumber", includeBuildNumber, "IncludeBuildNumber in package name") + flag.BoolVar(&includeBuildId, "includeBuildId", includeBuildId, "IncludeBuildId in package name") flag.BoolVar(&enterprise, "enterprise", enterprise, "Build enterprise version of Grafana") - flag.IntVar(&buildNumber, "buildNumber", 0, "Build number from CI system") + flag.StringVar(&buildIdRaw, "buildId", "0", "Build ID from CI system") flag.BoolVar(&isDev, "dev", isDev, "optimal for development, skips certain steps") flag.Parse() + buildId = shortenBuildId(buildIdRaw) + readVersionFromPackageJson() if pkgArch == "" { @@ -124,6 +128,8 @@ func main() { if goos == linux { createLinuxPackages() } + case "pkg-archive": + grunt(gruntBuildArg("package")...) case "pkg-rpm": grunt(gruntBuildArg("release")...) @@ -197,9 +203,9 @@ func readVersionFromPackageJson() { } // add timestamp to iteration - if includeBuildNumber { - if buildNumber != 0 { - linuxPackageIteration = fmt.Sprintf("%d%s", buildNumber, linuxPackageIteration) + if includeBuildId { + if buildId != "0" { + linuxPackageIteration = fmt.Sprintf("%s%s", buildId, linuxPackageIteration) } else { linuxPackageIteration = fmt.Sprintf("%d%s", time.Now().Unix(), linuxPackageIteration) } @@ -392,7 +398,7 @@ func grunt(params ...string) { func gruntBuildArg(task string) []string { args := []string{task} - if includeBuildNumber { + if includeBuildId { args = append(args, fmt.Sprintf("--pkgVer=%v-%v", linuxPackageVersion, linuxPackageIteration)) } else { args = append(args, fmt.Sprintf("--pkgVer=%v", version)) @@ -471,6 +477,7 @@ func ldflags() string { b.WriteString(fmt.Sprintf(" -X main.version=%s", version)) b.WriteString(fmt.Sprintf(" -X main.commit=%s", getGitSha())) b.WriteString(fmt.Sprintf(" -X main.buildstamp=%d", buildStamp())) + b.WriteString(fmt.Sprintf(" -X main.buildBranch=%s", getGitBranch())) return b.String() } @@ -518,6 +525,14 @@ func setBuildEnv() { } } +func getGitBranch() string { + v, err := runError("git", "rev-parse", "--abbrev-ref", "HEAD") + if err != nil { + return "master" + } + return string(v) +} + func getGitSha() string { v, err := runError("git", "rev-parse", "--short", "HEAD") if err != nil { @@ -623,3 +638,11 @@ func shaFile(file string) error { return out.Close() } + +func shortenBuildId(buildId string) string { + buildId = strings.Replace(buildId, "-", "", -1) + if len(buildId) < 9 { + return buildId + } + return buildId[0:8] +} diff --git a/conf/defaults.ini b/conf/defaults.ini index 750f06f2f6a..306c625d980 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -344,6 +344,7 @@ header_property = username auto_sign_up = true ldap_sync_ttl = 60 whitelist = +headers = #################################### Auth LDAP ########################### [auth.ldap] @@ -489,6 +490,10 @@ enabled = false enabled = true interval_seconds = 10 +#If both are set, basic auth will be required for the metrics endpoint. +basic_auth_username = +basic_auth_password = + # Send internal Grafana metrics to graphite [metrics.graphite] # Enable by setting the address setting (ex localhost:2003) @@ -557,3 +562,7 @@ callback_url = [panels] enable_alpha = false + +[enterprise] +license_path = + diff --git a/conf/sample.ini b/conf/sample.ini index e6a03718d19..c6b716a731d 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -294,6 +294,7 @@ log_queries = ;auto_sign_up = true ;ldap_sync_ttl = 60 ;whitelist = 192.168.1.1, 192.168.2.1 +;headers = Email:X-User-Email, Name:X-User-Name #################################### Basic Auth ########################## [auth.basic] @@ -475,3 +476,8 @@ log_queries = # Options to configure external image rendering server like https://github.com/grafana/grafana-image-renderer ;server_url = ;callback_url = + +[enterprise] +# Path to a valid Grafana Enterprise license.jwt file +;license_path = + diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json new file mode 100644 index 00000000000..8d0bae1221c --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -0,0 +1,511 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "gray", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "timeShift": null, + "title": "Business Hours", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Sunday's 20-23", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Each day of week", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": false, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "timeShift": null, + "title": "05:00", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-30d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Graph (Time Regions)", + "uid": "XMjIZPmik", + "version": 1 +} \ No newline at end of file diff --git a/devenv/dev-dashboards/panel_tests_table.json b/devenv/dev-dashboards/panel_tests_table.json index 8337e9cd746..ff0288c340a 100644 --- a/devenv/dev-dashboards/panel_tests_table.json +++ b/devenv/dev-dashboards/panel_tests_table.json @@ -404,6 +404,112 @@ "title": "Column style thresholds & units", "transform": "timeseries_to_columns", "type": "table" + }, + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 10, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 6, + "links": [], + "pageSize": 20, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkTargetBlank": true, + "linkTooltip": "", + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.5)", + "rgba(237, 129, 40, 0.5)", + "rgba(50, 172, 45, 0.5)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "link": true, + "linkUrl": "http://www.grafana.com", + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "ColorValue", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "null,1,20,90,30,5,0,20,10" + }, + { + "alias": "ColorCell", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "null,5,1,2,3,4,5,10,20" + } + ], + "title": "Column style thresholds and links", + "transform": "timeseries_to_columns", + "type": "table" } ], "refresh": false, @@ -449,5 +555,5 @@ "timezone": "browser", "title": "Panel Tests - Table", "uid": "pttable", - "version": 1 -} + "version": 2 +} \ No newline at end of file diff --git a/devenv/dev-dashboards/testdata_alerts.json b/devenv/dev-dashboards/testdata_alerts.json index 8c2edebf155..9f36638c012 100644 --- a/devenv/dev-dashboards/testdata_alerts.json +++ b/devenv/dev-dashboards/testdata_alerts.json @@ -1,250 +1,681 @@ { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 60 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "frequency": "60s", + "handler": 1, + "name": "TestData - Always OK", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 60 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Always OK", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": "125", + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "executionErrorState": "alerting", + "for": "0m", + "frequency": "60s", + "handler": 1, + "name": "TestData - Always Alerting", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Always Alerting", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "5m", + "frequency": "1m", + "handler": 1, + "name": "TestData - No data", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 5, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "no_data_points", + "stringInput": "", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 1 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "No data", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "1m", + "frequency": "1m", + "handler": 1, + "name": "TestData - Always Pending", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 6, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Always Alerting with For", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 100 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "900000h", + "frequency": "1m", + "handler": 1, + "name": "Always Pending", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 7, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 100 + } + ], + "timeFrom": null, + "timeRegions": [], + "timeShift": null, + "title": "Always Alerting with For", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], "revision": 2, - "title": "Alerting with TestData", + "schemaVersion": 16, + "style": "dark", "tags": [ "grafana-test" ], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": 255.625, - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 60 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always OK", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 3, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "1,20,90,30,5,0", - "target": "" - } - ], - "thresholds": [ - { - "value": 60, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always OK", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": "125", - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 177 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always Alerting", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 4, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "200,445,100,150,200,220,190", - "target": "" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 177 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always Alerting", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - } - ], + "templating": { + "list": [] + }, "time": { "from": "now-6h", "to": "now" @@ -274,14 +705,8 @@ "30d" ] }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 4, - "links": [], - "gnetId": null -} + "timezone": "browser", + "title": "Alerting with TestData", + "uid": "7MeksYbmk", + "version": 7 +} \ No newline at end of file diff --git a/devenv/docker/blocks/redis/docker-compose.yaml b/devenv/docker/blocks/redis/docker-compose.yaml new file mode 100644 index 00000000000..65071d4966b --- /dev/null +++ b/devenv/docker/blocks/redis/docker-compose.yaml @@ -0,0 +1,5 @@ + memcached: + image: redis:latest + ports: + - "6379:6379" + diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml index ce8630d88a4..1195e2a977c 100644 --- a/devenv/docker/ha_test/docker-compose.yaml +++ b/devenv/docker/ha_test/docker-compose.yaml @@ -9,7 +9,7 @@ services: - /var/run/docker.sock:/tmp/docker.sock:ro db: - image: mysql + image: mysql:5.6 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: grafana diff --git a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet index 86ded7e79d6..e9b8abfbb9c 100644 --- a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet +++ b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet @@ -39,6 +39,7 @@ local alertDashboardTemplate = { "executionErrorState": "alerting", "frequency": "10s", "handler": 1, + "for": "1m", "name": "bulk alerting", "noDataState": "no_data", "notifications": [ diff --git a/docs/sources/administration/permissions.md b/docs/sources/administration/permissions.md deleted file mode 100644 index 0d374f03647..00000000000 --- a/docs/sources/administration/permissions.md +++ /dev/null @@ -1,116 +0,0 @@ -+++ -title = "Permissions" -description = "Grafana user permissions" -keywords = ["grafana", "configuration", "documentation", "admin", "users", "permissions"] -type = "docs" -aliases = ["/reference/admin"] -[menu.docs] -name = "Permissions" -parent = "admin" -weight = 3 -+++ - -# Permissions - -Grafana users have permissions that are determined by their: - -- **Organization Role** (Admin, Editor, Viewer) -- Via **Team** memberships where the **Team** has been assigned specific permissions. -- Via permissions assigned directly to user (on folders or dashboards) -- The Grafana Admin (i.e. Super Admin) user flag. - -## Organization Roles - -Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do -in that organization. - -### Admin Role - -Can do everything scoped to the organization. For example: - -- Add & Edit data sources. -- Add & Edit organization users & teams. -- Configure App plugins & set org settings. - -### Editor Role - -- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. -- **Cannot** create or edit data sources nor invite new users. - -### Viewer Role - -- View any dashboard. This can be disabled on specific folders and dashboards. -- **Cannot** create or edit dashboards nor data sources. - -This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users -with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). -Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. - -## Grafana Admin - -This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. - -### Dashboard & Folder Permissions - -{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} - -For dashboards and dashboard folders there is a **Permissions** page that make it possible to -remove the default role based permissions for Editors and Viewers. It's here you can add and assign permissions to specific **Users** and **Teams**. - -You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. - -Permission levels: - -- **Admin**: Can edit & create dashboards and edit permissions. -- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. -- **View**: Can only view existing dashboards/folders. - -#### Restricting Access - -The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). - -- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. -- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. - -#### How Grafana Resolves Multiple Permissions - Examples - -##### Example 1 (`user1` has the Editor Role) - -Permissions for a dashboard: - -- `Everyone with Editor Role Can Edit` -- `user1 Can View` - -Result: `user1` has Edit permission as the highest permission always wins. - -##### Example 2 (`user1` has the Viewer Role and is a member of `team1`) - -Permissions for a dashboard: - -- `Everyone with Viewer Role Can View` -- `user1 Can Edit` -- `team1 Can Admin` - -Result: `user1` has Admin permission as the highest permission always wins. - -##### Example 3 - -Permissions for a dashboard: - -- `user1 Can Admin (inherited from parent folder)` -- `user1 Can Edit` - -Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. - -- **View**: Can only view existing dashboards/folders. -- You cannot override permissions for users with **Org Admin Role** -- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. - -### Data source permissions - -Permissions on dashboards and folders **do not** include permissions on data sources. A user with `Viewer` role -can still issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. -We hope to add permissions on data sources in a future release. Until then **do not** view dashboard permissions as a secure -way to restrict user data access. Dashboard permissions only limits what dashboards & folders a user can view & edit not which -data sources a user can access nor what queries a user can issue. - diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 9149aa42130..60e89b486a5 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -156,7 +156,7 @@ 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 | Elasticsearch | Elasticsearch version as a number (2/5/56) | +| esVersion | number | Elasticsearch | Elasticsearch version as a number (2/5/56/60) | | timeField | string | Elasticsearch | Which field that should be used as timestamp | | interval | string | Elasticsearch | Index date time format. nil(No Pattern), 'Hourly', 'Daily', 'Weekly', 'Monthly' or 'Yearly' | | authType | string | Cloudwatch | Auth provider. keys/credentials/arn | diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 488619055e2..036ff97056d 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of ## Rule Config -{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table** panels as well in a future release. @@ -48,6 +48,19 @@ panels as well in a future release. Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule. +### For + +> This setting is available in Grafana 5.4 and above. + +If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. + +Typically, it's always a good idea to use this setting since it's often worse to get false positive than wait a few minutes before the alert notification triggers. Looking at the `Alert list` or `Alert list panels` you will be able to see alerts in pending state. + +Below you can see an example timeline of an alert using the `For` setting. At ~16:04 the alert state changes to `Pending` and after 4 minutes it changes to `Alerting` which is when alert notifications are sent. Once the series falls back to normal the alert rule goes back to `OK`. +{{< imgbox img="/img/docs/v54/alerting-for-dark-theme.png" caption="Alerting For" >}} + +{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + ### Conditions Currently the only condition type that exists is a `Query` condition that allows you to @@ -57,11 +70,11 @@ specify a query letter, time range and an aggregation function. ### Query condition example ```sql -avg() OF query(A, 5m, now) IS BELOW 14 +avg() OF query(A, 15m, now) IS BELOW 14 ``` - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function. -- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. +- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 15 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. - `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold. The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially. diff --git a/docs/sources/auth/enhanced_ldap.md b/docs/sources/auth/enhanced_ldap.md new file mode 100644 index 00000000000..8eec57b1429 --- /dev/null +++ b/docs/sources/auth/enhanced_ldap.md @@ -0,0 +1,43 @@ ++++ +title = "Enhanced LDAP Integration" +description = "Grafana Enhanced LDAP Integration Guide " +keywords = ["grafana", "configuration", "documentation", "ldap", "active directory", "enterprise"] +type = "docs" +[menu.docs] +name = "Enhanced LDAP" +identifier = "enhanced-ldap" +parent = "authentication" +weight = 3 ++++ + +# Enhanced LDAP Integration + +> Enhanced LDAP Integration is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +The enhanced LDAP integration adds additional functionality on top of the [existing LDAP integration]({{< relref "auth/ldap.md" >}}). + +## LDAP Group Synchronization for Teams + +{{< docs-imagebox img="/img/docs/enterprise/team_members_ldap.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +With the enhanced LDAP integration it's possible to setup synchronization between LDAP groups and teams. This enables LDAP users which are members +of certain LDAP groups to automatically be added/removed as members to certain teams in Grafana. Currently the synchronization will only happen every +time a user logs in, but an active background synchronization is currently being developed. + +Grafana keeps track of all synchronized users in teams and you can see which users have been synchronized from LDAP in the team members list, see `LDAP` label in screenshot. +This mechanism allows Grafana to remove an existing synchronized user from a team when its LDAP group membership changes. This mechanism also enables you to manually add +a user as member of a team and it will not be removed when the user signs in. This gives you flexibility to combine LDAP group memberships and Grafana team memberships. + +
+ +### Enable LDAP group synchronization for a team + +{{< docs-imagebox img="/img/docs/enterprise/team_add_external_group.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" >}} + +1. Navigate to Configuration / Teams. +2. Select a team. +3. Select the External group sync tab and click on the `Add group` button. +4. Insert LDAP distinguished name (DN) of LDAP group you want to synchronize with the team. +5. Click on `Add group` button to save. + +
diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index a372600ac46..0480ee88adc 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings. ```bash [auth] -disable_login_form ⁼ true +disable_login_form = true +``` + +### Automatic OAuth login + +Set to true to attempt login with OAuth automatically, skipping the login screen. +This setting is ignored if multiple OAuth providers are configured. +Defaults to `false`. + +```bash +[auth] +oauth_auto_login = true ``` ### Hide sign-out menu @@ -84,3 +95,12 @@ Set to the option detailed below to true to hide sign-out menu link. Useful if y [auth] disable_signout_menu = true ``` + +### URL redirect after signing out + +URL to redirect the user to after signing out from Grafana. This can for example be used to enable signout from oauth provider. + +```bash +[auth] +signout_redirect_url = +``` diff --git a/docs/sources/enterprise/index.md b/docs/sources/enterprise/index.md new file mode 100644 index 00000000000..f65fa55f02b --- /dev/null +++ b/docs/sources/enterprise/index.md @@ -0,0 +1,67 @@ ++++ +title = "Grafana Enterprise" +description = "Grafana Enterprise overview" +keywords = ["grafana", "documentation", "datasource", "permissions", "ldap", "licensing", "enterprise"] +type = "docs" +[menu.docs] +name = "Grafana Enterprise" +identifier = "enterprise" +weight = 30 ++++ + +# Grafana Enterprise + +Grafana Enterprise is a commercial edition of Grafana that includes additional features not found in the open source +version. + +Building on everything you already know and love about Grafana, Grafana Enterprise adds premium data sources, +advanced authentication options, more permission controls, 24x7x365 support, and training from the core Grafana team. + +Grafana Enterprise includes all of the features found in the open source edition and more. + +___ + +### Enhanced LDAP Integration + +With Grafana Enterprise you can set up synchronization between LDAP Groups and Teams. [Learn More]({{< relref "auth/enhanced_ldap.md" >}}). + +### Datasource Permissions + +Datasource permissions allow you to restrict query access to only specific Teams and Users. [Learn More]({{< relref "permissions/datasource_permissions.md" >}}). + +### Premium Plugins + +With a Grafana Enterprise licence you will get access to premium plugins, including: + +* [Splunk](https://grafana.com/plugins/grafana-splunk-datasource) +* [AppDynamics](https://grafana.com/plugins/dlopes7-appdynamics-datasource) +* [DataDog](https://grafana.com/plugins/grafana-datadog-datasource) +* [Dynatrace](https://grafana.com/plugins/grafana-dynatrace-datasource) +* [New Relic](https://grafana.com/plugins/grafana-newrelic-datasource) + +## Try Grafana Enterprise + +You can learn more about Grafana Enterprise [here](https://grafana.com/enterprise). To purchase or obtain a trial license contact +the Grafana Labs [Sales Team](https://grafana.com/contact?about=support&topic=Grafana%20Enterprise). + +## License file management + +To download your Grafana Enterprise license log in to your [Grafana.com](https://grafana.com) account and go to your **Org +Profile**. In the side menu there is a section for Grafana Enterprise licenses. At the bottom of the license +details page there is **Download Token** link that will download the *license.jwt* file containing your license. + +Place the *license.jwt* file in Grafana's data folder. This is usually located at `/var/lib/grafana/data` on linux systems. + +You can also configure a custom location for the license file via the ini setting: + +```bash +[enterprise] +license_path = /company/secrets/license.jwt +``` + +This setting can also be set via ENV variable which is useful if you're running Grafana via docker and have a custom +volume where you have placed the license file. In this case set the ENV variable `GF_ENTERPRISE_LICENSE_PATH` to point +to the location of your license file. + + + diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index be36d108475..e2bcb50bb1d 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -60,7 +60,8 @@ Here is a minimal policy example: "Effect": "Allow", "Action": [ "cloudwatch:ListMetrics", - "cloudwatch:GetMetricStatistics" + "cloudwatch:GetMetricStatistics", + "cloudwatch:GetMetricData" ], "Resource": "*" }, diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 80a2f9a828a..aa60eb7cbc1 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -59,7 +59,7 @@ a time pattern for the index name or a wildcard. ### Elasticsearch version Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. -Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example. +Currently the versions available is 2.x, 5.x, 5.6+ or 6.0+. 5.6+ means a version of 5.6 or less than 6.0. 6.0+ means a version of 6.0 or higher, 6.3.2 for example. ### Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute. diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 988f632bff3..bc4e4df6cf9 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -73,6 +73,58 @@ Example: You can use wildcards (`*`) in place of database or table if you want to grant access to more databases and tables. +## Query Editor + +> Only available in Grafana v5.4+. + +{{< docs-imagebox img="/img/docs/v54/mysql_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v54/mysql_query.gif" >}} + +You find the MySQL query editor in the metrics tab in a panel's edit mode. You enter edit mode by clicking the +panel title, then edit. + +The query editor has a link named `Generated SQL` that shows up after a query has been executed, while in panel edit mode. Click on it and it will expand and show the raw interpolated SQL string that was executed. + +### Select table, time column and metric column (FROM) + +When you enter edit mode for the first time or add a new query Grafana will try to prefill the query builder with the first table that has a timestamp column and a numeric column. + +In the FROM field, Grafana will suggest tables that are in the configured database. To select a table or view in another database that your database user has access to you can manually enter a fully qualified name (database.table) like `otherDb.metrics`. + +The Time column field refers to the name of the column holding your time values. Selecting a value for the Metric column field is optional. If a value is selected, the Metric column field will be used as the series name. + +The metric column suggestions will only contain columns with a text datatype (text, tinytext, mediumtext, longtext, varchar, char). +If you want to use a column with a different datatype as metric column you may enter the column name with a cast: `CAST(numericColumn as CHAR)`. +You may also enter arbitrary SQL expressions in the metric column field that evaluate to a text datatype like +`CONCAT(column1, " ", CAST(numericColumn as CHAR))`. + +### Columns and Aggregation functions (SELECT) + +In the `SELECT` row you can specify what columns and functions you want to use. +In the column field you may write arbitrary expressions instead of a column name like `column1 * column2 / column3`. + +If you use aggregate functions you need to group your resultset. The editor will automatically add a `GROUP BY time` if you add an aggregate function. + +You may add further value columns by clicking the plus button and selecting `Column` from the menu. Multiple value columns will be plotted as separate series in the graph panel. + +### Filter data (WHERE) +To add a filter click the plus icon to the right of the `WHERE` condition. You can remove filters by clicking on +the filter and selecting `Remove`. A filter for the current selected timerange is automatically added to new queries. + +### Group By +To group by time or any other columns click the plus icon at the end of the GROUP BY row. The suggestion dropdown will only show text columns of your currently selected table but you may manually enter any column. +You can remove the group by clicking on the item and then selecting `Remove`. + +If you add any grouping, all selected columns need to have an aggregate function applied. The query builder will automatically add aggregate functions to all columns without aggregate functions when you add groupings. + +#### Gap Filling + +Grafana can fill in missing values when you group by time. The time function accepts two arguments. The first argument is the time window that you would like to group by, and the second argument is the value you want Grafana to fill missing items with. + +### Text Editor Mode (RAW) +You can switch to the raw query editor mode by clicking the hamburger icon and selecting `Switch editor mode` or by clicking `Edit SQL` below the query. + +> If you use the raw query editor, be sure your query at minimum has `ORDER BY time` and a filter on the returned time range. + ## Macros To simplify syntax and to allow for dynamic parts, like date range filters, the query can contain macros. diff --git a/docs/sources/features/datasources/stackdriver.md b/docs/sources/features/datasources/stackdriver.md index d19dbe4ea50..2c14d897d8e 100644 --- a/docs/sources/features/datasources/stackdriver.md +++ b/docs/sources/features/datasources/stackdriver.md @@ -158,9 +158,9 @@ Example Result: `compute.googleapis.com/instance/cpu/usage_time - server1-prod` It is also possible to resolve the name of the Monitored Resource Type. -| Alias Pattern Format | Description | Example Result | -| ------------------------ | ------------------------------------------------| ---------------- | -| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` | +| Alias Pattern Format | Description | Example Result | +| -------------------- | ----------------------------------------------- | -------------- | +| `{{resource.type}}` | returns the name of the monitored resource type | `gce_instance` | Example Alias By: `{{resource.type}} - {{metric.type}}` @@ -177,7 +177,17 @@ types of template variables. ### Query Variable -Writing variable queries is not supported yet. +Variable of the type *Query* allows you to query Stackdriver for various types of data. The Stackdriver data source plugin provides the following `Query Types`. + +| Name | Description | +| ------------------- | ------------------------------------------------------------------------------------------------- | +| *Metric Types* | Returns a list of metric type names that are available for the specified service. | +| *Labels Keys* | Returns a list of keys for `metric label` and `resource label` in the specified metric. | +| *Labels Values* | Returns a list of values for the label in the specified metric. | +| *Resource Types* | Returns a list of resource types for the the specified metric. | +| *Aggregations* | Returns a list of aggregations (cross series reducers) for the the specified metric. | +| *Aligners* | Returns a list of aligners (per series aligners) for the the specified metric. | +| *Alignment periods* | Returns a list of all alignment periods that are available in Stackdriver query editor in Grafana | ### Using variables in queries diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md index 5a010ceca40..44fa0e7c0db 100644 --- a/docs/sources/features/panels/graph.md +++ b/docs/sources/features/panels/graph.md @@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when the graph crosses a particular threshold. +### Time Regions + +> Only available in Grafana v5.4 and above. + +{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}} + +Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours. + ## Time Range {{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}} diff --git a/docs/sources/guides/whats-new-in-v5-3.md b/docs/sources/guides/whats-new-in-v5-3.md index 5dcadc0813d..10592f51648 100644 --- a/docs/sources/guides/whats-new-in-v5-3.md +++ b/docs/sources/guides/whats-new-in-v5-3.md @@ -18,7 +18,7 @@ Grafana v5.3 brings new features, many enhancements and bug fixes. This article - [TV mode]({{< relref "#tv-and-kiosk-mode" >}}) is improved and more accessible - [Alerting]({{< relref "#notification-reminders" >}}) with notification reminders - [Postgres]({{< relref "#postgres-query-builder" >}}) gets a new query builder! -- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for Gitlab is improved +- [OAuth]({{< relref "#improved-oauth-support-for-gitlab" >}}) support for GitLab is improved - [Annotations]({{< relref "#annotations" >}}) with template variable filtering - [Variables]({{< relref "#variables" >}}) with free text support @@ -69,9 +69,9 @@ Grafana 5.3 comes with a new graphical query builder for Postgres. This brings P {{< docs-imagebox img="/img/docs/v53/postgres_query_still.png" class="docs-image--no-shadow" animated-gif="/img/docs/v53/postgres_query.gif" >}} -## Improved OAuth Support for Gitlab +## Improved OAuth Support for GitLab -Grafana 5.3 comes with a new OAuth integration for Gitlab that enables configuration to only allow users that are a member of certain Gitlab groups to authenticate. This makes it possible to use Gitlab OAuth with Grafana in a shared environment without giving everyone access to Grafana. +Grafana 5.3 comes with a new OAuth integration for GitLab that enables configuration to only allow users that are a member of certain GitLab groups to authenticate. This makes it possible to use GitLab OAuth with Grafana in a shared environment without giving everyone access to Grafana. Learn how to enable and configure it in the [documentation](/auth/gitlab/). ## Annotations diff --git a/docs/sources/guides/whats-new-in-v5-4.md b/docs/sources/guides/whats-new-in-v5-4.md new file mode 100644 index 00000000000..e0a3fd5c4ba --- /dev/null +++ b/docs/sources/guides/whats-new-in-v5-4.md @@ -0,0 +1,18 @@ ++++ +title = "What's New in Grafana v5.4" +description = "Feature & improvement highlights for Grafana v5.4" +keywords = ["grafana", "new", "documentation", "5.4"] +type = "docs" +[menu.docs] +name = "Version 5.4" +identifier = "v5.4" +parent = "whatsnew" +weight = -10 ++++ + +# What's New in Grafana v5.4 + +## Changelog + +Checkout the [CHANGELOG.md](https://github.com/grafana/grafana/blob/master/CHANGELOG.md) file for a complete list +of new features, changes, and bug fixes. diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 103de190793..2d70a6d2017 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -290,7 +290,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "sendReminder": true, "frequency": "15m", "settings": { - "addresses: "carl@grafana.com;dev@grafana.com" + "addresses": "carl@grafana.com;dev@grafana.com" } } ``` diff --git a/docs/sources/http_api/datasource_permissions.md b/docs/sources/http_api/datasource_permissions.md new file mode 100644 index 00000000000..226beac3728 --- /dev/null +++ b/docs/sources/http_api/datasource_permissions.md @@ -0,0 +1,249 @@ ++++ +title = "Datasource Permissions HTTP API " +description = "Grafana Datasource Permissions HTTP API" +keywords = ["grafana", "http", "documentation", "api", "datasource", "permission", "permissions", "acl", "enterprise"] +aliases = ["/http_api/datasourcepermissions/"] +type = "docs" +[menu.docs] +name = "Datasource Permissions" +parent = "http_api" ++++ + +# Datasource Permissions API + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +This API can be used to enable, disable, list, add and remove permissions for a datasource. + +Permissions can be set for a user or a team. Permissions cannot be set for Admins - they always have access to everything. + +The permission levels for the permission field: + +- 1 = Query + +## Enable permissions for a datasource + +`POST /api/datasources/:id/enable-permissions` + +Enables permissions for the datasource with the given `id`. No one except Org Admins will be able to query the datasource until permissions have been added which permit certain users or teams to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/enable-permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permissions enabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be enabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Disable permissions for a datasource + +`POST /api/datasources/:id/disable-permissions` + +Disables permissions for the datasource with the given `id`. All existing permissions will be removed and anyone will be able to query the datasource. + +**Example request**: + +```http +POST /api/datasources/1/disable-permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permissions disabled"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permissions cannot be disabled, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Get permissions for a datasource + +`GET /api/datasources/:id/permissions` + +Gets all existing permissions for the datasource with the given `id`. + +**Example request**: + +```http +GET /api/datasources/1/permissions HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response** + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 551 + +{ + "datasourceId": 1, + "enabled": true, + "permissions": + [ + { + "id": 1, + "datasourceId": 1, + "userId": 1, + "userLogin": "user", + "userEmail": "user@test.com", + "userAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + }, + { + "id": 2, + "datasourceId": 1, + "teamId": 1, + "team": "A Team", + "teamAvatarUrl": "/avatar/46d229b033af06a191ff2267bca9ae56", + "permission": 1, + "permissionName": "Query", + "created": "2017-06-20T02:00:00+02:00", + "updated": "2017-06-20T02:00:00+02:00", + } + ] +} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Add permission for a datasource + +`POST /api/datasources/:id/permissions` + +Adds a user permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "userId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Adds a team permission for the datasource with the given `id`. + +**Example request**: + +```http +POST /api/datasources/1/permissions +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "teamId": 1, + "permission": 1 +} +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission added"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Permission cannot be added, see response body for details +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found + +## Remove permission for a datasource + +`DELETE /api/datasources/:id/permissions/:permissionId` + +Removes the permission with the given `permissionId` for the datasource with the given `id`. + +**Example request**: + +```http +DELETE /api/datasources/1/permissions/2 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example response**: + +```http +HTTP/1.1 200 OK +Content-Type: application/json; charset=UTF-8 +Content-Length: 35 + +{"message":"Datasource permission removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Access denied +- **404** - Datasource not found or permission not found diff --git a/docs/sources/http_api/external_group_sync.md b/docs/sources/http_api/external_group_sync.md new file mode 100644 index 00000000000..2ce06c2c94e --- /dev/null +++ b/docs/sources/http_api/external_group_sync.md @@ -0,0 +1,111 @@ ++++ +title = "External Group Sync HTTP API " +description = "Grafana External Group Sync HTTP API" +keywords = ["grafana", "http", "documentation", "api", "team", "teams", "group", "member", "enterprise"] +aliases = ["/http_api/external_group_sync/"] +type = "docs" +[menu.docs] +name = "External Group Sync" +parent = "http_api" ++++ + +# External Group Synchronization API + +> External Group Synchronization is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +## Get External Groups + +`GET /api/teams/:teamId/groups` + +**Example Request**: + +```http +GET /api/teams/1/groups HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "orgId": 1, + "teamId": 1, + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" + } +] +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied + +## Add External Group + +`POST /api/teams/:teamId/groups` + +**Example Request**: + +```http +POST /api/teams/1/members HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= + +{ + "groupId": "cn=editors,ou=groups,dc=grafana,dc=org" +} +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Group added to Team"} +``` + +Status Codes: + +- **200** - Ok +- **400** - Group is already added to this team +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found + +## Remove External Group + +`DELETE /api/teams/:teamId/groups/:groupId` + +**Example Request**: + +```http +DELETE /api/teams/1/groups/cn=editors,ou=groups,dc=grafana,dc=org HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{"message":"Team Group removed"} +``` + +Status Codes: + +- **200** - Ok +- **401** - Unauthorized +- **403** - Permission denied +- **404** - Team not found/Group not found diff --git a/docs/sources/http_api/team.md b/docs/sources/http_api/team.md index 94ea4108481..5024ec69522 100644 --- a/docs/sources/http_api/team.md +++ b/docs/sources/http_api/team.md @@ -30,7 +30,7 @@ Authorization: Basic YWRtaW46YWRtaW4= ### Using the query parameter -Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. +Default value for the `perpage` parameter is `1000` and for the `page` parameter is `1`. The `totalCount` field in the response can be used for pagination of the teams list E.g. if `totalCount` is equal to 100 teams and the `perpage` parameter is set to 10 then there are 10 pages of teams. @@ -314,3 +314,67 @@ Status Codes: - **401** - Unauthorized - **403** - Permission denied - **404** - Team not found/Team member not found + +## Get Team Preferences + +`GET /api/teams/:teamId/preferences` + +**Example Request**: + +```http +GET /api/teams/2/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +{ + "theme": "", + "homeDashboardId": 0, + "timezone": "" +} +``` + +## Update Team Preferences + +`PUT /api/teams/:teamId/preferences` + +**Example Request**: + +```http +PUT /api/teams/2/preferences HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + +{ + "theme": "dark", + "homeDashboardId": 39, + "timezone": "utc" +} +``` + +JSON Body Schema: + +- **theme** - One of: ``light``, ``dark``, or an empty string for the default theme +- **homeDashboardId** - The numerical ``:id`` of a dashboard, default: ``0`` +- **timezone** - One of: ``utc``, ``browser``, or an empty string for the default + +Omitting a key will cause the current value to be replaced with the system default value. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: text/plain; charset=utf-8 + +{ + "message":"Preferences updated" +} +``` diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index b9047187b2d..047d4473603 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -226,6 +226,40 @@ Content-Type: application/json ] ``` +## Get Teams for user + +`GET /api/users/:id/teams` + +**Example Request**: + +```http +GET /api/users/1/teams HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Basic YWRtaW46YWRtaW4= +``` + +Requires basic authentication and that the authenticated user is a Grafana Admin. + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id":1, + "orgId":1, + "name":"team1", + "email":"", + "avatarUrl":"/avatar/3fcfe295eae3bcb67a49349377428a66", + "memberCount":1 + } +] +``` + + ## User ## Actual User diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 8d156e739bf..30ef020a3de 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -454,6 +454,12 @@ Ex `filters = sqlstore:debug` ### enabled Enable metrics reporting. defaults true. Available via HTTP API `/metrics`. +### basic_auth_username +If set configures the username to use for basic authentication on the metrics endpoint. + +### basic_auth_password +If set configures the password to use for basic authentication on the metrics endpoint. + ### interval_seconds Flush/Write interval when sending metrics to external TSDB. Defaults to 10s. diff --git a/docs/sources/permissions/dashboard_folder_permissions.md b/docs/sources/permissions/dashboard_folder_permissions.md new file mode 100644 index 00000000000..83cb0ee86a3 --- /dev/null +++ b/docs/sources/permissions/dashboard_folder_permissions.md @@ -0,0 +1,73 @@ ++++ +title = "Dashboard & Folder Permissions" +description = "Grafana Dashboard & Folder Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "dashboard", "folder", "permissions", "teams"] +type = "docs" +[menu.docs] +name = "Dashboard & Folder" +identifier = "dashboard-folder-permissions" +parent = "permissions" +weight = 3 ++++ + +# Dashboard & Folder Permissions + +{{< docs-imagebox img="/img/docs/v50/folder_permissions.png" max-width="500px" class="docs-image--right" >}} + +For dashboards and dashboard folders there is a **Permissions** page that make it possible to +remove the default role based permissions for Editors and Viewers. On this page you can add and assign permissions to specific **Users** and **Teams**. + +You can assign & remove permissions for **Organization Roles**, **Users** and **Teams**. + +Permission levels: + +- **Admin**: Can edit & create dashboards and edit permissions. +- **Edit**: Can edit & create dashboards. **Cannot** edit folder/dashboard permissions. +- **View**: Can only view existing dashboards/folders. + +## Restricting Access + +The highest permission always wins so if you for example want to hide a folder or dashboard from others you need to remove the **Organization Role** based permission from the Access Control List (ACL). + +- You cannot override permissions for users with the **Org Admin Role**. Admins always have access to everything. +- A more specific permission with a lower permission level will not have any effect if a more general rule exists with higher permission level. You need to remove or lower the permission level of the more general rule. + +### How Grafana Resolves Multiple Permissions - Examples + +#### Example 1 (`user1` has the Editor Role) + +Permissions for a dashboard: + +- `Everyone with Editor Role Can Edit` +- `user1 Can View` + +Result: `user1` has Edit permission as the highest permission always wins. + +#### Example 2 (`user1` has the Viewer Role and is a member of `team1`) + +Permissions for a dashboard: + +- `Everyone with Viewer Role Can View` +- `user1 Can Edit` +- `team1 Can Admin` + +Result: `user1` has Admin permission as the highest permission always wins. + +#### Example 3 + +Permissions for a dashboard: + +- `user1 Can Admin (inherited from parent folder)` +- `user1 Can Edit` + +Result: You cannot override to a lower permission. `user1` has Admin permission as the highest permission always wins. + +## Summary + +- **View**: Can only view existing dashboards/folders. +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. + +For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. +- You cannot override permissions for users with **Org Admin Role** +- A more specific permission with lower permission level will not have any effect if a more general rule exists with higher permission level. For example if "Everyone with Editor Role Can Edit" exists in the ACL list then **John Doe** will still have Edit permission even after you have specifically added a permission for this user with the permission set to **View**. You need to remove or lower the permission level of the more general rule. diff --git a/docs/sources/permissions/datasource_permissions.md b/docs/sources/permissions/datasource_permissions.md new file mode 100644 index 00000000000..ec54c1fbccd --- /dev/null +++ b/docs/sources/permissions/datasource_permissions.md @@ -0,0 +1,71 @@ ++++ +title = "Datasource Permissions" +description = "Grafana Datasource Permissions Guide " +keywords = ["grafana", "configuration", "documentation", "datasource", "permissions", "users", "teams", "enterprise"] +type = "docs" +[menu.docs] +name = "Datasource" +identifier = "datasource-permissions" +parent = "permissions" +weight = 4 ++++ + +# Datasource Permissions + +> Datasource Permissions is only available in Grafana Enterprise. Read more about [Grafana Enterprise]({{< relref "enterprise/index.md" >}}). + +Datasource permissions allows you to restrict access for users to query a datasource. For each datasource there is +a permission page that makes it possible to enable permissions and restrict query permissions to specific +**Users** and **Teams**. + +## Restricting Access - Enable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_enable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_enable.gif" >}} + +By default, permissions are disabled for datasources and a datasource in an organization can be queried by any user in +that organization. For example a user with `Viewer` role can still issue any possible query to a datasource, not just +those queries that exist on dashboards he/she has access to. + +When permissions are enabled for a datasource in an organization you will restrict admin and query access for that +datasource to [admin users](/permissions/organization_roles/#admin-role) in that organization. + +**To enable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to enable permissions for. +3. Select the Permissions tab and click on the `Enable` button. + +
+ +## Allow users and teams to query a datasource + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_add_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_add.gif" >}} + +After you have [enabled permissions](#restricting-access-enable-permissions) for a datasource you can assign query +permissions to users and teams which will allow access to query the datasource. + +**Assign query permission to users and teams:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to assign query permissions for. +3. Select the Permissions tab. +4. click on the `Add Permission` button. +5. Select Team/User and find the team/user you want to allow query access and click on the `Save` button. + +
+ +## Restore Default Access - Disable Permissions + +{{< docs-imagebox img="/img/docs/enterprise/datasource_permissions_disable_still.png" class="docs-image--no-shadow docs-image--right" max-width= "600px" animated-gif="/img/docs/enterprise/datasource_permissions_disable.gif" >}} + +If you have enabled permissions for a datasource and want to return datasource permissions to the default, i.e. +datasource can be queried by any user in that organization, you can disable permissions with a click of a button. +Note that all existing permissions created for datasource will be deleted. + +**To disable permissions for a datasource:** + +1. Navigate to Configuration / Data Sources. +2. Select the datasource you want to disable permissions for. +3. Select the Permissions tab and click on the `Disable Permissions` button. + +
diff --git a/docs/sources/permissions/index.md b/docs/sources/permissions/index.md new file mode 100644 index 00000000000..42514f76baf --- /dev/null +++ b/docs/sources/permissions/index.md @@ -0,0 +1,12 @@ ++++ +title = "Permissions" +description = "Permissions" +type = "docs" +[menu.docs] +name = "Permissions" +identifier = "permissions" +parent = "admin" +weight = 3 ++++ + + diff --git a/docs/sources/permissions/organization_roles.md b/docs/sources/permissions/organization_roles.md new file mode 100644 index 00000000000..626d79fad87 --- /dev/null +++ b/docs/sources/permissions/organization_roles.md @@ -0,0 +1,38 @@ ++++ +title = "Organization Roles" +description = "Grafana Organization Roles Guide " +keywords = ["grafana", "configuration", "documentation", "organization", "roles", "permissions"] +type = "docs" +[menu.docs] +name = "Organization Roles" +identifier = "organization-roles" +parent = "permissions" +weight = 2 ++++ + +# Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. + +## Admin Role + +Can do everything scoped to the organization. For example: + +- Add & Edit data sources. +- Add & Edit organization users & teams. +- Configure App plugins & set org settings. + +## Editor Role + +- Can create and modify dashboards & alert rules. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit data sources nor invite new users. + +## Viewer Role + +- View any dashboard. This can be disabled on specific folders and dashboards. +- **Cannot** create or edit dashboards nor data sources. + +This role can be tweaked via Grafana server setting [viewers_can_edit]({{< relref "installation/configuration.md#viewers-can-edit" >}}). If you set this to true users +with **Viewer** can also make transient dashboard edits, meaning they can modify panels & queries but not save the changes (nor create new dashboards). +Useful for public Grafana installations where you want anonymous users to be able to edit panels & queries but not save or create new dashboards. diff --git a/docs/sources/permissions/overview.md b/docs/sources/permissions/overview.md new file mode 100644 index 00000000000..cd3fc5417b6 --- /dev/null +++ b/docs/sources/permissions/overview.md @@ -0,0 +1,42 @@ ++++ +title = "Overview" +description = "Overview for permissions" +keywords = ["grafana", "configuration", "documentation", "admin", "users", "datasources", "permissions"] +type = "docs" +aliases = ["/reference/admin", "/administration/permissions/"] +[menu.docs] +name = "Overview" +identifier = "overview-permissions" +parent = "permissions" +weight = 1 ++++ + +# Permissions Overview + +Grafana users have permissions that are determined by their: + +- **Organization Role** (Admin, Editor, Viewer) +- Via **Team** memberships where the **Team** has been assigned specific permissions. +- Via permissions assigned directly to user (on folders, dashboards, datasources) +- The Grafana Admin (i.e. Super Admin) user flag. + +## Grafana Admin + +This admin flag makes a user a `Super Admin`. This means they can access the `Server Admin` views where all users and organizations can be administrated. + +## Organization Roles + +Users can be belong to one or more organizations. A user's organization membership is tied to a role that defines what the user is allowed to do +in that organization. Learn more about [Organization Roles]({{< relref "permissions/organization_roles.md" >}}). + + +## Dashboard & Folder Permissions + +Dashboard and folder permissions allows you to remove the default role based permissions for Editors and Viewers and assign permissions to specific **Users** and **Teams**. Learn more about [Dashboard & Folder Permissions]({{< relref "permissions/dashboard_folder_permissions.md" >}}). + +## Datasource Permissions + +Per default, a datasource in an organization can be queried by any user in that organization. For example a user with `Viewer` role can still +issue any possible query to a data source, not just those queries that exist on dashboards he/she has access to. + +Datasource permissions allows you to change the default permissions for datasources and restrict query permissions to specific **Users** and **Teams**. Read more about [Datasource Permissions]({{< relref "permissions/datasource_permissions.md" >}}). diff --git a/docs/sources/reference/scripting.md b/docs/sources/reference/scripting.md index 7f218765d39..12ab91f3c3c 100644 --- a/docs/sources/reference/scripting.md +++ b/docs/sources/reference/scripting.md @@ -12,7 +12,7 @@ weight = 9 If you have lots of metric names that change (new servers etc) in a defined pattern it is irritating to constantly have to create new dashboards. -With scripted dashboards you can dynamically create your dashboards using javascript. In the folder grafana install folder +With scripted dashboards you can dynamically create your dashboards using javascript. In the grafana install folder under `public/dashboards/` there is a file named `scripted.js`. This file contains an example of a scripted dashboard. You can access it by using the url: `http://grafana_url/dashboard/script/scripted.js?rows=3&name=myName` diff --git a/docs/sources/whatsnew/index.md b/docs/sources/whatsnew/index.md index df472f07093..f4159643d72 100644 --- a/docs/sources/whatsnew/index.md +++ b/docs/sources/whatsnew/index.md @@ -3,7 +3,7 @@ title = "What's New in Grafana" [menu.docs] name = "What's New In Grafana" identifier = "whatsnew" -weight = 3 +weight = 5 +++ diff --git a/latest.json b/latest.json index 992af0a8336..5e72c11c1f2 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "5.3.2", - "testing": "5.3.2" + "stable": "5.3.4", + "testing": "5.3.4" } diff --git a/package.json b/package.json index 27bbe0271d1..77218960b38 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~1.0.2", "grunt-exec": "^1.0.1", + "grunt-newer": "^1.3.0", "grunt-notify": "^0.4.5", "grunt-postcss": "^0.8.0", "grunt-sass": "^2.0.0", diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 567da94f881..5c1d9c8271a 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function checkUser() { if [ `id -u` -ne 0 ]; then diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index acd2a360a93..b1e2e387e4d 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=deb \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/packaging/docker/build-enterprise.sh b/packaging/docker/build-enterprise.sh index f716a1f44f1..e10e0d691e8 100755 --- a/packaging/docker/build-enterprise.sh +++ b/packaging/docker/build-enterprise.sh @@ -1,10 +1,20 @@ #!/bin/sh set -e -_grafana_tag=$1 +_raw_grafana_tag=$1 _docker_repo=${2:-grafana/grafana-enterprise} +if echo "$_raw_grafana_tag" | grep -q "^v"; then + _grafana_tag=$(echo "${_raw_grafana_tag}" | cut -d "v" -f 2) +else + _grafana_tag="${_raw_grafana_tag}" +fi + +echo "Building and deploying ${_docker_repo}:${_grafana_tag}" + docker build \ --tag "${_docker_repo}:${_grafana_tag}"\ --no-cache=true \ . + +docker push "${_docker_repo}:${_grafana_tag}" diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index bc001bdf90a..63f20742c96 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -80,6 +80,7 @@ fi exec grafana-server \ --homepath="$GF_PATHS_HOME" \ --config="$GF_PATHS_CONFIG" \ + --packaging=docker \ "$@" \ cfg:default.log.mode="console" \ cfg:default.paths.data="$GF_PATHS_DATA" \ diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index cefe212116c..b7b41e58e8d 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -60,7 +60,7 @@ fi # overwrite settings from default file [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function isRunning() { status -p $PID_FILE $NAME > /dev/null 2>&1 diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index f228c8d8b14..ad5006d1d4c 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=rpm \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a936d696207..66b3b504946 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -134,12 +134,16 @@ func AlertTest(c *m.ReqContext, dto dtos.AlertTestCommand) Response { OrgId: c.OrgId, Dashboard: dto.Dashboard, PanelId: dto.PanelId, + User: c.SignedInUser, } if err := bus.Dispatch(&backendCmd); err != nil { if validationErr, ok := err.(alerting.ValidationError); ok { return Error(422, validationErr.Error(), nil) } + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", err) + } return Error(500, "Failed to test rule", err) } @@ -291,7 +295,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response { return Error(500, "", err) } - var response m.AlertStateType = m.AlertStatePending + var response m.AlertStateType = m.AlertStateUnknown pausedState := "un-paused" if cmd.Paused { response = m.AlertStatePaused diff --git a/pkg/api/api.go b/pkg/api/api.go index f1fe940e416..0526ee80afe 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -140,6 +140,7 @@ func (hs *HTTPServer) registerRoutes() { usersRoute.Get("/", Wrap(SearchUsers)) usersRoute.Get("/search", Wrap(SearchUsersWithPaging)) usersRoute.Get("/:id", Wrap(GetUserByID)) + usersRoute.Get("/:id/teams", Wrap(GetUserTeams)) usersRoute.Get("/:id/orgs", Wrap(GetUserOrgList)) // query parameters /users/lookup?loginOrEmail=admin@example.com usersRoute.Get("/lookup", Wrap(GetUserByLoginOrEmail)) @@ -155,6 +156,8 @@ func (hs *HTTPServer) registerRoutes() { teamsRoute.Get("/:teamId/members", Wrap(GetTeamMembers)) teamsRoute.Post("/:teamId/members", bind(m.AddTeamMemberCommand{}), Wrap(AddTeamMember)) teamsRoute.Delete("/:teamId/members/:userId", Wrap(RemoveTeamMember)) + teamsRoute.Get("/:teamId/preferences", Wrap(GetTeamPreferences)) + teamsRoute.Put("/:teamId/preferences", bind(dtos.UpdatePrefsCmd{}), Wrap(UpdateTeamPreferences)) }, reqOrgAdmin) // team without requirement of user to be org admin @@ -242,7 +245,7 @@ func (hs *HTTPServer) registerRoutes() { apiRoute.Get("/datasources/id/:name", Wrap(GetDataSourceIdByName), reqSignedIn) - apiRoute.Get("/plugins", Wrap(GetPluginList)) + apiRoute.Get("/plugins", Wrap(hs.GetPluginList)) apiRoute.Get("/plugins/:pluginId/settings", Wrap(GetPluginSettingByID)) apiRoute.Get("/plugins/:pluginId/markdown/:name", Wrap(GetPluginMarkdown)) diff --git a/pkg/api/basic_auth.go b/pkg/api/basic_auth.go new file mode 100644 index 00000000000..376cfb24c91 --- /dev/null +++ b/pkg/api/basic_auth.go @@ -0,0 +1,19 @@ +package api + +import ( + "crypto/subtle" + macaron "gopkg.in/macaron.v1" +) + +// BasicAuthenticatedRequest parses the provided HTTP request for basic authentication credentials +// and returns true if the provided credentials match the expected username and password. +// Returns false if the request is unauthenticated. +// Uses constant-time comparison in order to mitigate timing attacks. +func BasicAuthenticatedRequest(req macaron.Request, expectedUser, expectedPass string) bool { + user, pass, ok := req.BasicAuth() + if !ok || subtle.ConstantTimeCompare([]byte(user), []byte(expectedUser)) != 1 || subtle.ConstantTimeCompare([]byte(pass), []byte(expectedPass)) != 1 { + return false + } + + return true +} diff --git a/pkg/api/basic_auth_test.go b/pkg/api/basic_auth_test.go new file mode 100644 index 00000000000..0b5051c3e2a --- /dev/null +++ b/pkg/api/basic_auth_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "encoding/base64" + "fmt" + "net/http" + "testing" + + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/macaron.v1" +) + +func TestBasicAuthenticatedRequest(t *testing.T) { + expectedUser := "prometheus" + expectedPass := "password" + + Convey("Given a valid set of basic auth credentials", t, func() { + httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + So(err, ShouldBeNil) + req := macaron.Request{ + Request: httpReq, + } + encodedCreds := encodeBasicAuthCredentials(expectedUser, expectedPass) + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) + authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) + So(authenticated, ShouldBeTrue) + }) + + Convey("Given an invalid set of basic auth credentials", t, func() { + httpReq, err := http.NewRequest("GET", "http://localhost:3000/metrics", nil) + So(err, ShouldBeNil) + req := macaron.Request{ + Request: httpReq, + } + encodedCreds := encodeBasicAuthCredentials("invaliduser", "invalidpass") + req.Header.Add("Authorization", fmt.Sprintf("Basic %s", encodedCreds)) + authenticated := BasicAuthenticatedRequest(req, expectedUser, expectedPass) + So(authenticated, ShouldBeFalse) + }) +} + +func encodeBasicAuthCredentials(user, pass string) string { + creds := fmt.Sprintf("%s:%s", user, pass) + return base64.StdEncoding.EncodeToString([]byte(creds)) +} diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index 02248334b9c..6abb72f1559 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -293,7 +293,7 @@ func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response { } func GetHomeDashboard(c *m.ReqContext) Response { - prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser} if err := bus.Dispatch(&prefsQuery); err != nil { return Error(500, "Failed to get preferences", err) } diff --git a/pkg/api/dataproxy.go b/pkg/api/dataproxy.go index 3bb2f236129..5cde0efd0b4 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -1,62 +1,22 @@ package api import ( - "fmt" - "github.com/pkg/errors" - "time" - "github.com/grafana/grafana/pkg/api/pluginproxy" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" ) -const HeaderNameNoBackendCache = "X-Grafana-NoCache" - -func (hs *HTTPServer) getDatasourceFromCache(id int64, c *m.ReqContext) (*m.DataSource, error) { - userPermissionsQuery := m.GetDataSourcePermissionsForUserQuery{ - User: c.SignedInUser, - } - if err := bus.Dispatch(&userPermissionsQuery); err != nil { - if err != bus.ErrHandlerNotFound { - return nil, err - } - } else { - permissionType, exists := userPermissionsQuery.Result[id] - if exists && permissionType != m.DsPermissionQuery { - return nil, errors.New("User not allowed to access datasource") - } - } - - nocache := c.Req.Header.Get(HeaderNameNoBackendCache) == "true" - cacheKey := fmt.Sprintf("ds-%d", id) - - if !nocache { - if cached, found := hs.cache.Get(cacheKey); found { - ds := cached.(*m.DataSource) - if ds.OrgId == c.OrgId { - return ds, nil - } - } - } - - query := m.GetDataSourceByIdQuery{Id: id, OrgId: c.OrgId} - if err := bus.Dispatch(&query); err != nil { - return nil, err - } - - hs.cache.Set(cacheKey, query.Result, time.Second*5) - return query.Result, nil -} - func (hs *HTTPServer) ProxyDataSourceRequest(c *m.ReqContext) { c.TimeRequest(metrics.M_DataSource_ProxyReq_Timer) dsId := c.ParamsInt64(":id") - ds, err := hs.getDatasourceFromCache(dsId, c) - + ds, err := hs.DatasourceCache.GetDatasource(dsId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + c.JsonApiErr(403, "Access denied to datasource", err) + return + } c.JsonApiErr(500, "Unable to load datasource meta data", err) return } diff --git a/pkg/api/dtos/index.go b/pkg/api/dtos/index.go index 77004899fc3..bd3ac76eec8 100644 --- a/pkg/api/dtos/index.go +++ b/pkg/api/dtos/index.go @@ -14,6 +14,7 @@ type IndexViewData struct { NewGrafanaVersionExists bool NewGrafanaVersion string AppName string + AppNameBodyClass string } type PluginCss struct { diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 78a611c5eeb..edc9d96d1ac 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -19,9 +19,9 @@ type PluginSetting struct { JsonData map[string]interface{} `json:"jsonData"` DefaultNavUrl string `json:"defaultNavUrl"` - LatestVersion string `json:"latestVersion"` - HasUpdate bool `json:"hasUpdate"` - State string `json:"state"` + LatestVersion string `json:"latestVersion"` + HasUpdate bool `json:"hasUpdate"` + State plugins.PluginState `json:"state"` } type PluginListItem struct { @@ -34,7 +34,7 @@ type PluginListItem struct { LatestVersion string `json:"latestVersion"` HasUpdate bool `json:"hasUpdate"` DefaultNavUrl string `json:"defaultNavUrl"` - State string `json:"state"` + State plugins.PluginState `json:"state"` } type PluginList []PluginListItem diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 1de65c7b2ce..65affe83e98 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -133,7 +133,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf panels := map[string]interface{}{} for _, panel := range enabledPlugins.Panels { - if panel.State == "alpha" && !hs.Cfg.EnableAlphaPanels { + if panel.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels { continue } diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 858b3c5a8c5..d4d7b41bec5 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -16,7 +16,6 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" - gocache "github.com/patrickmn/go-cache" macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/api/live" @@ -28,6 +27,8 @@ 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/cache" + "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -46,19 +47,19 @@ type HTTPServer struct { macaron *macaron.Macaron context context.Context streamManager *live.StreamManager - cache *gocache.Cache httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` } func (hs *HTTPServer) Init() error { hs.log = log.New("http.server") - hs.cache = gocache.New(5*time.Minute, 10*time.Minute) hs.streamManager = live.NewStreamManager() hs.macaron = hs.newMacaron() @@ -231,6 +232,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() { m.Use(middleware.ValidateHostHeader(setting.Domain)) } + m.Use(middleware.HandleNoCacheHeader()) m.Use(middleware.AddDefaultResponseHeaders()) } @@ -243,6 +245,11 @@ func (hs *HTTPServer) metricsEndpoint(ctx *macaron.Context) { return } + if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) { + ctx.Resp.WriteHeader(http.StatusUnauthorized) + return + } + promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}). ServeHTTP(ctx.Resp, ctx.Req.Request) } @@ -297,3 +304,7 @@ func (hs *HTTPServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, }, )) } + +func (hs *HTTPServer) metricsEndpointBasicAuthEnabled() bool { + return hs.Cfg.MetricsEndpointBasicAuthUsername != "" && hs.Cfg.MetricsEndpointBasicAuthPassword != "" +} diff --git a/pkg/api/http_server_test.go b/pkg/api/http_server_test.go new file mode 100644 index 00000000000..0f99ae82db5 --- /dev/null +++ b/pkg/api/http_server_test.go @@ -0,0 +1,30 @@ +package api + +import ( + "testing" + + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestHTTPServer(t *testing.T) { + Convey("Given a HTTPServer", t, func() { + ts := &HTTPServer{ + Cfg: setting.NewCfg(), + } + + Convey("Given that basic auth on the metrics endpoint is enabled", func() { + ts.Cfg.MetricsEndpointBasicAuthUsername = "foo" + ts.Cfg.MetricsEndpointBasicAuthPassword = "bar" + + So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeTrue) + }) + + Convey("Given that basic auth on the metrics endpoint is disabled", func() { + ts.Cfg.MetricsEndpointBasicAuthUsername = "" + ts.Cfg.MetricsEndpointBasicAuthPassword = "" + + So(ts.metricsEndpointBasicAuthEnabled(), ShouldBeFalse) + }) + }) +} diff --git a/pkg/api/index.go b/pkg/api/index.go index e61620f9586..253fa9c17af 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -23,7 +23,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er return nil, err } - prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} + prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser} if err := bus.Dispatch(&prefsQuery); err != nil { return nil, err } @@ -83,6 +83,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er NewGrafanaVersion: plugins.GrafanaLatestVersion, NewGrafanaVersionExists: plugins.GrafanaHasUpdate, AppName: setting.ApplicationName, + AppNameBodyClass: getAppNameBodyClass(setting.ApplicationName), } if setting.DisableGravatar { @@ -377,3 +378,14 @@ func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) { c.HTML(404, "index", data) } + +func getAppNameBodyClass(name string) string { + switch name { + case setting.APP_NAME: + return "app-grafana" + case setting.APP_NAME_ENTERPRISE: + return "app-enterprise" + default: + return "" + } +} diff --git a/pkg/api/login.go b/pkg/api/login.go index 1083f89adfd..05afc40e59a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { viewData.Settings["loginError"] = loginError } + if tryOAuthAutoLogin(c) { + return + } + if !tryLoginUsingRememberCookie(c) { c.HTML(200, ViewIndex, viewData) return @@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { c.Redirect(setting.AppSubUrl + "/") } +func tryOAuthAutoLogin(c *m.ReqContext) bool { + if !setting.OAuthAutoLogin { + return false + } + oauthInfos := setting.OAuthService.OAuthInfos + if len(oauthInfos) != 1 { + log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.") + return false + } + for key := range setting.OAuthService.OAuthInfos { + redirectUrl := setting.AppSubUrl + "/login/" + key + log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl) + c.Redirect(redirectUrl, 307) + return true + } + return false +} + func tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index cb80bd346b8..6e5ae0f8761 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -25,8 +25,11 @@ func (hs *HTTPServer) QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) R return Error(400, "Query missing datasourceId", nil) } - ds, err := hs.getDatasourceFromCache(datasourceId, c) + ds, err := hs.DatasourceCache.GetDatasource(datasourceId, c.SignedInUser, c.SkipCache) if err != nil { + if err == m.ErrDataSourceAccessDenied { + return Error(403, "Access denied to datasource", err) + } return Error(500, "Unable to load datasource meta data", err) } diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index 0c000058e4b..38a2fd187e3 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -195,6 +195,10 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { req.Header.Del("X-Forwarded-Proto") req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) + // Clear Origin and Referer to avoir CORS issues + req.Header.Del("Origin") + req.Header.Del("Referer") + // set X-Forwarded-For header if req.RemoteAddr != "" { remoteAddr, _, err := net.SplitHostPort(req.RemoteAddr) diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index ab0effb298f..c9be169565f 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -371,13 +371,22 @@ func TestDSRouteRule(t *testing.T) { ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "/path/to/folder/") req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + req.Header.Add("Origin", "grafana.com") + req.Header.Add("Referer", "grafana.com") + req.Header.Add("X-Canary", "stillthere") So(err, ShouldBeNil) proxy.getDirector()(req) - Convey("Shoudl keep user request (including trailing slash)", func() { + Convey("Should keep user request (including trailing slash)", func() { So(req.URL.String(), ShouldEqual, "http://host/root/path/to/folder/") }) + + Convey("Origin and Referer headers should be dropped", func() { + So(req.Header.Get("Origin"), ShouldEqual, "") + So(req.Header.Get("Referer"), ShouldEqual, "") + So(req.Header.Get("X-Canary"), ShouldEqual, "stillthere") + }) }) }) } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 4b44009ab8c..455420e4688 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/setting" ) -func GetPluginList(c *m.ReqContext) Response { +func (hs *HTTPServer) GetPluginList(c *m.ReqContext) Response { typeFilter := c.Query("type") enabledFilter := c.Query("enabled") embeddedFilter := c.Query("embedded") @@ -39,6 +39,10 @@ func GetPluginList(c *m.ReqContext) Response { continue } + if pluginDef.State == plugins.PluginStateAlpha && !hs.Cfg.EnableAlphaPanels { + continue + } + listItem := dtos.PluginListItem{ Id: pluginDef.Id, Name: pluginDef.Name, diff --git a/pkg/api/preferences.go b/pkg/api/preferences.go index 26695130975..9b451aa2a6e 100644 --- a/pkg/api/preferences.go +++ b/pkg/api/preferences.go @@ -21,11 +21,11 @@ func SetHomeDashboard(c *m.ReqContext, cmd m.SavePreferencesCommand) Response { // GET /api/user/preferences func GetUserPreferences(c *m.ReqContext) Response { - return getPreferencesFor(c.OrgId, c.UserId) + return getPreferencesFor(c.OrgId, c.UserId, 0) } -func getPreferencesFor(orgID int64, userID int64) Response { - prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID} +func getPreferencesFor(orgID, userID, teamID int64) Response { + prefsQuery := m.GetPreferencesQuery{UserId: userID, OrgId: orgID, TeamId: teamID} if err := bus.Dispatch(&prefsQuery); err != nil { return Error(500, "Failed to get preferences", err) @@ -42,13 +42,14 @@ func getPreferencesFor(orgID int64, userID int64) Response { // PUT /api/user/preferences func UpdateUserPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { - return updatePreferencesFor(c.OrgId, c.UserId, &dtoCmd) + return updatePreferencesFor(c.OrgId, c.UserId, 0, &dtoCmd) } -func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd) Response { +func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsCmd) Response { saveCmd := m.SavePreferencesCommand{ UserId: userID, OrgId: orgID, + TeamId: teamId, Theme: dtoCmd.Theme, Timezone: dtoCmd.Timezone, HomeDashboardId: dtoCmd.HomeDashboardID, @@ -63,10 +64,10 @@ func updatePreferencesFor(orgID int64, userID int64, dtoCmd *dtos.UpdatePrefsCmd // GET /api/org/preferences func GetOrgPreferences(c *m.ReqContext) Response { - return getPreferencesFor(c.OrgId, 0) + return getPreferencesFor(c.OrgId, 0, 0) } // PUT /api/org/preferences func UpdateOrgPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { - return updatePreferencesFor(c.OrgId, 0, &dtoCmd) + return updatePreferencesFor(c.OrgId, 0, 0, &dtoCmd) } diff --git a/pkg/api/team.go b/pkg/api/team.go index ebb426c4c82..32265e5d018 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -96,3 +96,13 @@ func GetTeamByID(c *m.ReqContext) Response { query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return JSON(200, &query.Result) } + +// GET /api/teams/:teamId/preferences +func GetTeamPreferences(c *m.ReqContext) Response { + return getPreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId")) +} + +// PUT /api/teams/:teamId/preferences +func UpdateTeamPreferences(c *m.ReqContext, dtoCmd dtos.UpdatePrefsCmd) Response { + return updatePreferencesFor(c.OrgId, 0, c.ParamsInt64(":teamId"), &dtoCmd) +} diff --git a/pkg/api/user.go b/pkg/api/user.go index 7116ad83f3f..6db9c6f6baf 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -113,7 +113,16 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response { // GET /api/user/teams func GetSignedInUserTeamList(c *m.ReqContext) Response { - query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId} + return getUserTeamList(c.OrgId, c.UserId) +} + +// GET /api/users/:id/teams +func GetUserTeams(c *m.ReqContext) Response { + return getUserTeamList(c.OrgId, c.ParamsInt64(":id")) +} + +func getUserTeamList(userID int64, orgID int64) Response { + query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID} if err := bus.Dispatch(&query); err != nil { return Error(500, "Failed to get user teams", err) @@ -122,11 +131,10 @@ func GetSignedInUserTeamList(c *m.ReqContext) Response { for _, team := range query.Result { team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) } - return JSON(200, query.Result) } -// GET /api/user/:id/orgs +// GET /api/users/:id/orgs func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) } diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 06c07a2887c..3bdaf0cc80e 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -3,6 +3,8 @@ package main import ( "flag" "fmt" + "net/http" + _ "net/http/pprof" "os" "os/signal" "runtime" @@ -11,16 +13,12 @@ import ( "syscall" "time" - "net/http" - _ "net/http/pprof" - + "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/setting" - - extensions "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" + "github.com/grafana/grafana/pkg/setting" _ "github.com/grafana/grafana/pkg/tsdb/cloudwatch" _ "github.com/grafana/grafana/pkg/tsdb/elasticsearch" _ "github.com/grafana/grafana/pkg/tsdb/graphite" @@ -35,11 +33,13 @@ import ( var version = "5.0.0" var commit = "NA" +var buildBranch = "master" var buildstamp string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var pidFile = flag.String("pidfile", "", "path to pid file") +var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed") func main() { v := flag.Bool("v", false, "prints current version and exits") @@ -47,14 +47,17 @@ func main() { profilePort := flag.Int("profile-port", 6060, "Define custom port for profiling") flag.Parse() if *v { - fmt.Printf("Version %s (commit: %s)\n", version, commit) + fmt.Printf("Version %s (commit: %s, branch: %s)\n", version, commit, buildBranch) os.Exit(0) } if *profile { runtime.SetBlockProfileRate(1) go func() { - http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil) + err := http.ListenAndServe(fmt.Sprintf("localhost:%d", *profilePort), nil) + if err != nil { + panic(err) + } }() f, err := os.Create("trace.out") @@ -78,9 +81,11 @@ func main() { setting.BuildVersion = version setting.BuildCommit = commit setting.BuildStamp = buildstampInt64 + setting.BuildBranch = buildBranch setting.IsEnterprise = extensions.IsEnterprise + setting.Packaging = validPackaging(*packaging) - metrics.M_Grafana_Version.WithLabelValues(version).Set(1) + metrics.SetBuildInformation(version, commit, buildBranch) server := NewGrafanaServer() @@ -95,6 +100,16 @@ func main() { os.Exit(code) } +func validPackaging(packaging string) string { + validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"} + for _, vt := range validTypes { + if packaging == vt { + return packaging + } + } + return "unknown" +} + func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) sighupChan := make(chan os.Signal, 1) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 8794d7d8338..d9fd1e62ce7 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -12,20 +12,20 @@ import ( "time" "github.com/facebookgo/inject" + "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/social" "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/login" + "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/social" - // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/metrics" @@ -67,6 +67,7 @@ type GrafanaServerImpl struct { } func (g *GrafanaServerImpl) Run() error { + var err error g.loadConfiguration() g.writePIDFile() @@ -74,19 +75,38 @@ func (g *GrafanaServerImpl) Run() error { social.NewOAuthService() serviceGraph := inject.Graph{} - serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) - serviceGraph.Provide(&inject.Object{Value: g.cfg}) - serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) + err = serviceGraph.Provide(&inject.Object{Value: bus.GetBus()}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } + err = serviceGraph.Provide(&inject.Object{Value: g.cfg}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } + err = serviceGraph.Provide(&inject.Object{Value: routing.NewRouteRegister(middleware.RequestMetrics, middleware.RequestTracing)}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } + err = serviceGraph.Provide(&inject.Object{Value: cache.New(5*time.Minute, 10*time.Minute)}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } // self registered services services := registry.GetServices() // Add all services to dependency graph for _, service := range services { - serviceGraph.Provide(&inject.Object{Value: service.Instance}) + err = serviceGraph.Provide(&inject.Object{Value: service.Instance}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } } - serviceGraph.Provide(&inject.Object{Value: g}) + err = serviceGraph.Provide(&inject.Object{Value: g}) + if err != nil { + return fmt.Errorf("Failed to provide object to the graph: %v", err) + } // Inject dependencies to services if err := serviceGraph.Populate(); err != nil { @@ -159,7 +179,7 @@ func (g *GrafanaServerImpl) loadConfiguration() { os.Exit(1) } - g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "compiled", time.Unix(setting.BuildStamp, 0)) + g.log.Info("Starting "+setting.ApplicationName, "version", version, "commit", commit, "branch", buildBranch, "compiled", time.Unix(setting.BuildStamp, 0)) g.cfg.LogConfigSources() } diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 991fa72fd54..b766d963328 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -2,6 +2,7 @@ package login import ( "errors" + "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 9a514fdb6f3..326514a9687 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -58,7 +58,14 @@ var ( M_StatActive_Users prometheus.Gauge M_StatTotal_Orgs prometheus.Gauge M_StatTotal_Playlists prometheus.Gauge - M_Grafana_Version *prometheus.GaugeVec + + // M_Grafana_Version is a gauge that contains build info about this binary + // + // Deprecated: use M_Grafana_Build_Version instead. + M_Grafana_Version *prometheus.GaugeVec + + // grafanaBuildVersion is a gauge that contains build info about this binary + grafanaBuildVersion *prometheus.GaugeVec ) func newCounterVecStartingAtZero(opts prometheus.CounterOpts, labels []string, labelValues ...string) *prometheus.CounterVec { @@ -293,9 +300,25 @@ func init() { M_Grafana_Version = prometheus.NewGaugeVec(prometheus.GaugeOpts{ Name: "info", - Help: "Information about the Grafana", + Help: "Information about the Grafana. This metric is deprecated. please use `grafana_build_info`", Namespace: exporterName, }, []string{"version"}) + + grafanaBuildVersion = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "build_info", + Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.", + Namespace: exporterName, + }, []string{"version", "revision", "branch", "goversion"}) +} + +// SetBuildInformation sets the build information for this binary +func SetBuildInformation(version, revision, branch string) { + // We export this info twice for backwards compatibility. + // Once this have been released for some time we should be able to remote `M_Grafana_Version` + // The reason we added a new one is that its common practice in the prometheus community + // to name this metric `*_build_info` so its easy to do aggregation on all programs. + M_Grafana_Version.WithLabelValues(version).Set(1) + grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1) } func initMetricVars() { @@ -334,7 +357,8 @@ func initMetricVars() { M_StatActive_Users, M_StatTotal_Orgs, M_StatTotal_Playlists, - M_Grafana_Version) + M_Grafana_Version, + grafanaBuildVersion) } @@ -373,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) { metrics := map[string]interface{}{} report := map[string]interface{}{ - "version": version, - "metrics": metrics, - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "edition": getEdition(), + "version": version, + "metrics": metrics, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "edition": getEdition(), + "packaging": setting.Packaging, } statsQuery := models.GetSystemStatsQuery{} @@ -423,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) { } metrics["stats.ds.other.count"] = dsOtherCount + metrics["stats.packaging."+setting.Packaging+".count"] = 1 + dsAccessStats := models.GetDataSourceAccessStatsQuery{} if err := bus.Dispatch(&dsAccessStats); err != nil { metricsLogger.Error("Failed to get datasource access stats", "error", err) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 43739221f1e..c27d6f64b8c 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) { setting.BasicAuthEnabled = true setting.LdapEnabled = true setting.AuthProxyEnabled = true + setting.Packaging = "deb" wg.Add(1) sendUsageStats(oauthProviders) @@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) + + So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1) }) }) diff --git a/pkg/middleware/headers.go b/pkg/middleware/headers.go new file mode 100644 index 00000000000..28c623d74b0 --- /dev/null +++ b/pkg/middleware/headers.go @@ -0,0 +1,14 @@ +package middleware + +import ( + m "github.com/grafana/grafana/pkg/models" + macaron "gopkg.in/macaron.v1" +) + +const HeaderNameNoBackendCache = "X-Grafana-NoCache" + +func HandleNoCacheHeader() macaron.Handler { + return func(ctx *m.ReqContext) { + ctx.SkipCache = ctx.Req.Header.Get(HeaderNameNoBackendCache) == "true" + } +} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 7b29901c1a3..ace72d998eb 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -29,6 +29,7 @@ func GetContextHandler() macaron.Handler { Session: session.GetSession(), IsSignedIn: false, AllowAnonymous: false, + SkipCache: false, Logger: log.New("context"), } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 1830b3eb161..e9a3c8059f8 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -18,6 +18,7 @@ import ( ) func TestMiddlewareContext(t *testing.T) { + setting.ERR_TEMPLATE_NAME = "error-template" Convey("Given the grafana middleware", t, func() { middlewareScenario("middleware should add context to injector", func(sc *scenarioContext) { diff --git a/pkg/middleware/recovery.go b/pkg/middleware/recovery.go index 456bc91354e..b8ca85fb45b 100644 --- a/pkg/middleware/recovery.go +++ b/pkg/middleware/recovery.go @@ -115,6 +115,7 @@ func Recovery() macaron.Handler { c.Data["Title"] = "Server Error" c.Data["AppSubUrl"] = setting.AppSubUrl + c.Data["Theme"] = setting.DefaultTheme if setting.Env == setting.DEV { if theErr, ok := err.(error); ok { @@ -138,7 +139,7 @@ func Recovery() macaron.Handler { c.JSON(500, resp) } else { - c.HTML(500, "error") + c.HTML(500, setting.ERR_TEMPLATE_NAME) } } }() diff --git a/pkg/middleware/recovery_test.go b/pkg/middleware/recovery_test.go index 4bbedbc3b21..c92150f3b7d 100644 --- a/pkg/middleware/recovery_test.go +++ b/pkg/middleware/recovery_test.go @@ -8,11 +8,14 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/session" + "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" "gopkg.in/macaron.v1" ) func TestRecoveryMiddleware(t *testing.T) { + setting.ERR_TEMPLATE_NAME = "error-template" + Convey("Given an api route that panics", t, func() { apiURL := "/api/whatever" recoveryScenario("recovery middleware should return json", apiURL, func(sc *scenarioContext) { @@ -50,6 +53,7 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) { sc := &scenarioContext{ url: url, } + viewsPath, _ := filepath.Abs("../../public/views") sc.m = macaron.New() diff --git a/pkg/models/alert.go b/pkg/models/alert.go index ba1fc0779ba..e8171f8cf0b 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -19,6 +19,7 @@ const ( AlertStateAlerting AlertStateType = "alerting" AlertStateOK AlertStateType = "ok" AlertStatePending AlertStateType = "pending" + AlertStateUnknown AlertStateType = "unknown" ) const ( @@ -39,7 +40,12 @@ var ( ) func (s AlertStateType) IsValid() bool { - return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending + return s == AlertStateOK || + s == AlertStateNoData || + s == AlertStatePaused || + s == AlertStatePending || + s == AlertStateAlerting || + s == AlertStateUnknown } func (s NoDataOption) IsValid() bool { @@ -66,12 +72,13 @@ type Alert struct { PanelId int64 Name string Message string - Severity string + Severity string //Unused State AlertStateType - Handler int64 + Handler int64 //Unused Silenced bool ExecutionError string Frequency int64 + For time.Duration EvalData *simplejson.Json NewStateDate time.Time @@ -215,13 +222,14 @@ type AlertStateInfoDTO struct { // "Internal" commands type UpdateDashboardAlertsCommand struct { - UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } type ValidateDashboardAlertsCommand struct { UserId int64 OrgId int64 Dashboard *Dashboard + User *SignedInUser } diff --git a/pkg/models/context.go b/pkg/models/context.go index 262f6550954..7cb80a957c3 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -20,6 +20,7 @@ type ReqContext struct { IsSignedIn bool IsRenderCall bool AllowAnonymous bool + SkipCache bool Logger log.Logger } @@ -36,7 +37,7 @@ func (ctx *ReqContext) Handle(status int, title string, err error) { ctx.Data["AppSubUrl"] = setting.AppSubUrl ctx.Data["Theme"] = "dark" - ctx.HTML(status, "error") + ctx.HTML(status, setting.ERR_TEMPLATE_NAME) } func (ctx *ReqContext) JsonOK(message string) { diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index b71d17ec0d1..89439420d7a 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -207,11 +207,6 @@ func (p DsPermissionType) String() string { return names[int(p)] } -type GetDataSourcePermissionsForUserQuery struct { - User *SignedInUser - Result map[int64]DsPermissionType -} - type DatasourcesPermissionFilterQuery struct { User *SignedInUser Datasources []*DataSource diff --git a/pkg/models/preferences.go b/pkg/models/preferences.go index 4c77bc96d4d..c73e0be4949 100644 --- a/pkg/models/preferences.go +++ b/pkg/models/preferences.go @@ -14,6 +14,7 @@ type Preferences struct { Id int64 OrgId int64 UserId int64 + TeamId int64 Version int HomeDashboardId int64 Timezone string @@ -29,14 +30,13 @@ type GetPreferencesQuery struct { Id int64 OrgId int64 UserId int64 + TeamId int64 Result *Preferences } type GetPreferencesWithDefaultsQuery struct { - Id int64 - OrgId int64 - UserId int64 + User *SignedInUser Result *Preferences } @@ -46,6 +46,7 @@ type GetPreferencesWithDefaultsQuery struct { type SavePreferencesCommand struct { UserId int64 OrgId int64 + TeamId int64 HomeDashboardId int64 `json:"homeDashboardId"` Timezone string `json:"timezone"` diff --git a/pkg/models/user.go b/pkg/models/user.go index d5b912e0a9c..e3c7b556d35 100644 --- a/pkg/models/user.go +++ b/pkg/models/user.go @@ -165,6 +165,7 @@ type SignedInUser struct { IsAnonymous bool HelpFlags1 HelpFlags1 LastSeenAt time.Time + Teams []int64 } func (u *SignedInUser) ShouldUpdateLastSeenAt() bool { diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 9677c21ef04..5ac436205c1 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -17,6 +17,13 @@ var ( PluginTypeDashboard = "dashboard" ) +type PluginState string + +var ( + PluginStateAlpha PluginState = "alpha" + PluginStateBeta PluginState = "beta" +) + type PluginNotFoundError struct { PluginId string } @@ -39,7 +46,7 @@ type PluginBase struct { Module string `json:"module"` BaseUrl string `json:"baseUrl"` HideFromList bool `json:"hideFromList,omitempty"` - State string `json:"state,omitempty"` + State PluginState `json:"state,omitempty"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 87fca27f6c1..487a6db7927 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -29,11 +29,42 @@ func Register(descriptor *Descriptor) { } func GetServices() []*Descriptor { - sort.Slice(services, func(i, j int) bool { - return services[i].InitPriority > services[j].InitPriority + slice := getServicesWithOverrides() + + sort.Slice(slice, func(i, j int) bool { + return slice[i].InitPriority > slice[j].InitPriority }) - return services + return slice +} + +type OverrideServiceFunc func(descriptor Descriptor) (*Descriptor, bool) + +var overrides []OverrideServiceFunc + +func RegisterOverride(fn OverrideServiceFunc) { + overrides = append(overrides, fn) +} + +func getServicesWithOverrides() []*Descriptor { + slice := []*Descriptor{} + for _, s := range services { + var descriptor *Descriptor + for _, fn := range overrides { + if newDescriptor, override := fn(*s); override { + descriptor = newDescriptor + break + } + } + + if descriptor != nil { + slice = append(slice, descriptor) + } else { + slice = append(slice, s) + } + } + + return slice } // Service interface is the lowest common shape that services diff --git a/pkg/services/alerting/commands.go b/pkg/services/alerting/commands.go index 02186d697ee..dd2ff5658d6 100644 --- a/pkg/services/alerting/commands.go +++ b/pkg/services/alerting/commands.go @@ -11,7 +11,7 @@ func init() { } func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) return extractor.ValidateAlerts() } @@ -19,11 +19,11 @@ func validateDashboardAlerts(cmd *m.ValidateDashboardAlertsCommand) error { func updateDashboardAlerts(cmd *m.UpdateDashboardAlertsCommand) error { saveAlerts := m.SaveAlertsCommand{ OrgId: cmd.OrgId, - UserId: cmd.UserId, + UserId: cmd.User.UserId, DashboardId: cmd.Dashboard.Id, } - extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId) + extractor := NewDashAlertExtractor(cmd.Dashboard, cmd.OrgId, cmd.User) alerts, err := extractor.GetAlerts() if err != nil { diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 9d4e1462690..7f11fc498bd 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -52,6 +52,24 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(1)) }) + Convey("median should ignore null values", func() { + reducer := NewSimpleReducer("median") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 3)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(1)), 4)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(2)), 5)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(float64(3)), 6)) + + result := reducer.Reduce(series) + So(result.Valid, ShouldEqual, true) + So(result.Float64, ShouldEqual, float64(2)) + }) + Convey("avg", func() { result := testReducer("avg", 1, 2, 3) So(result, ShouldEqual, float64(2)) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..4db942e0a55 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -68,8 +68,13 @@ func (c *EvalContext) GetStateModel() *StateDescription { Color: "#D63232", Text: "Alerting", } + case m.AlertStateUnknown: + return &StateDescription{ + Color: "#888888", + Text: "Unknown", + } default: - panic("Unknown rule state " + c.Rule.State) + panic("Unknown rule state for alert " + c.Rule.State) } } @@ -113,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } +// GetNewState returns the new state from the alert rule evaluation func (c *EvalContext) GetNewState() m.AlertStateType { + ns := getNewStateInternal(c) + if ns != m.AlertStateAlerting || c.Rule.For == 0 { + return ns + } + + since := time.Since(c.Rule.LastStateChange) + if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For { + return m.AlertStateAlerting + } + + if c.PrevAlertState == m.AlertStateAlerting { + return m.AlertStateAlerting + } + + return m.AlertStatePending +} + +func getNewStateInternal(c *EvalContext) m.AlertStateType { if c.Error != nil { c.log.Error("Alert Rule Result Error", "ruleId", c.Rule.Id, @@ -125,11 +149,13 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.PrevAlertState } return c.Rule.ExecutionErrorState.ToAlertState() + } - } else if c.Firing { + if c.Firing { return m.AlertStateAlerting + } - } else if c.NoDataFound { + if c.NoDataFound { c.log.Info("Alert Rule returned no data", "ruleId", c.Rule.Id, "name", c.Rule.Name, diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 750fa959683..af7e66b2f07 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -2,11 +2,11 @@ package alerting import ( "context" - "fmt" + "errors" "testing" + "time" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" ) func TestStateIsUpdatedWhenNeeded(t *testing.T) { @@ -31,71 +31,176 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) { }) } -func TestAlertingEvalContext(t *testing.T) { - Convey("Should compute and replace properly new rule state", t, func() { +func TestGetStateFromEvalContext(t *testing.T) { + tcs := []struct { + name string + expected models.AlertStateType + applyFn func(ec *EvalContext) + }{ + { + name: "ok -> alerting", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.Firing = true + ec.PrevAlertState = models.AlertStateOK + }, + }, + { + name: "ok -> error(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + }, + }, + { + name: "ok -> pending. since its been firing for less than FOR", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + ec.Rule.For = time.Minute * 5 + }, + }, + { + name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.For = time.Minute * 2 + }, + }, + { + name: "pending -> alerting. since its been firing for more than FOR and prev state is pending", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.For = time.Minute * 2 + }, + }, + { + name: "alerting -> alerting. should not update regardless of FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateAlerting + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.For = time.Minute * 2 + }, + }, + { + name: "ok -> ok. should not update regardless of FOR", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.For = time.Minute * 2 + }, + }, + { + name: "ok -> error(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "pending -> error(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "ok -> no_data(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + }, + }, + { + name: "ok -> no_data(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + { + name: "pending -> no_data(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + { + name: "pending -> no_data(alerting) with for duration have not passed", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> no_data(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, + { + name: "pending -> error(alerting) with for duration have not passed ", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> error(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, + } + + for _, tc := range tcs { ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - dummieError := fmt.Errorf("dummie error") - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Firing = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - - Convey("ok -> no_data(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataSetAlerting - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - }) + tc.applyFn(ctx) + have := ctx.GetNewState() + if have != tc.expected { + t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have)) + } + } } diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index edfab2dedee..e33e3dc2af3 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,8 +2,8 @@ package alerting import ( "errors" - "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -13,14 +13,16 @@ import ( // DashAlertExtractor extracts alerts from the dashboard json type DashAlertExtractor struct { + User *m.SignedInUser Dash *m.Dashboard OrgID int64 log log.Logger } // NewDashAlertExtractor returns a new DashAlertExtractor -func NewDashAlertExtractor(dash *m.Dashboard, orgID int64) *DashAlertExtractor { +func NewDashAlertExtractor(dash *m.Dashboard, orgID int64, user *m.SignedInUser) *DashAlertExtractor { return &DashAlertExtractor{ + User: user, Dash: dash, OrgID: orgID, log: log.New("alerting.extractor"), @@ -113,6 +115,15 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } + rawFor := jsonAlert.Get("for").MustString() + var forValue time.Duration + if rawFor != "" { + forValue, err = time.ParseDuration(rawFor) + if err != nil { + return nil, ValidationError{Reason: "Could not parse for"} + } + } + alert := &m.Alert{ DashboardId: e.Dash.Id, OrgId: e.OrgID, @@ -122,6 +133,7 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, + For: forValue, } for _, condition := range jsonAlert.Get("conditions").MustArray() { @@ -149,6 +161,21 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: fmt.Sprintf("Data source used by alert rule not found, alertName=%v, datasource=%s", alert.Name, dsName)} } + dsFilterQuery := m.DatasourcesPermissionFilterQuery{ + User: e.User, + Datasources: []*m.DataSource{datasource}, + } + + if err := bus.Dispatch(&dsFilterQuery); err != nil { + if err != bus.ErrHandlerNotFound { + return nil, err + } + } else { + if len(dsFilterQuery.Result) == 0 { + return nil, m.ErrDataSourceAccessDenied + } + } + jsonQuery.SetPath([]string{"datasourceId"}, datasource.Id) if interval, err := panel.Get("interval").String(); err == nil { diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index e2dc01a1181..9665a657bb7 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -3,6 +3,7 @@ package alerting import ( "io/ioutil" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - json, err := ioutil.ReadFile("./test-data/graphite-alert.json") + json, err := ioutil.ReadFile("./testdata/graphite-alert.json") So(err, ShouldBeNil) Convey("Extractor should not modify the original json", func() { @@ -69,7 +70,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(getTarget(dashJson), ShouldEqual, "") }) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, _ = extractor.GetAlerts() Convey("Dashboard json should not be updated after extracting rules", func() { @@ -83,7 +84,7 @@ func TestAlertRuleExtraction(t *testing.T) { So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) { So(alerts[1].PanelId, ShouldEqual, 4) }) + Convey("should extract for param", func() { + So(alerts[0].For, ShouldEqual, time.Minute*2) + So(alerts[1].For, ShouldEqual, time.Duration(0)) + }) + Convey("should extract name and desc", func() { So(alerts[0].Name, ShouldEqual, "name1") So(alerts[0].Message, ShouldEqual, "desc1") @@ -140,13 +146,13 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panels missing id should return error", func() { - panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json") + panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithoutId) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -156,13 +162,13 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panel with id set to zero should return error", func() { - panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json") + panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithIdZero) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) _, err = extractor.GetAlerts() @@ -172,13 +178,13 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse alerts from dashboard without rows", func() { - json, err := ioutil.ReadFile("./test-data/v5-dashboard.json") + json, err := ioutil.ReadFile("./testdata/v5-dashboard.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -192,13 +198,13 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard containing influxdb alert", func() { - json, err := ioutil.ReadFile("./test-data/influxdb-alert.json") + json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -221,14 +227,14 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Should be able to extract collapsed panels", func() { - json, err := ioutil.ReadFile("./test-data/collapsed-panels.json") + json, err := ioutil.ReadFile("./testdata/collapsed-panels.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJson) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) alerts, err := extractor.GetAlerts() @@ -242,13 +248,13 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard without id and containing an alert", func() { - json, err := ioutil.ReadFile("./test-data/dash-without-id.json") + json, err := ioutil.ReadFile("./testdata/dash-without-id.json") So(err, ShouldBeNil) dashJSON, err := simplejson.NewJson(json) So(err, ShouldBeNil) dash := m.NewDashboardFromJson(dashJSON) - extractor := NewDashAlertExtractor(dash, 1) + extractor := NewDashAlertExtractor(dash, 1, nil) err = extractor.ValidateAlerts() diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index 3549b536e48..7510742ed17 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -1,13 +1,60 @@ package notifiers import ( + "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) +func TestWhenAlertManagerShouldNotify(t *testing.T) { + tcs := []struct { + prevState m.AlertStateType + newState m.AlertStateType + + expect bool + }{ + { + prevState: m.AlertStatePending, + newState: m.AlertStateOK, + expect: false, + }, + { + prevState: m.AlertStateAlerting, + newState: m.AlertStateOK, + expect: true, + }, + { + prevState: m.AlertStateOK, + newState: m.AlertStatePending, + expect: false, + }, + { + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, + expect: false, + }, + } + + for _, tc := range tcs { + am := &AlertmanagerNotifier{log: log.New("test.logger")} + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.prevState, + }) + + evalContext.Rule.State = tc.newState + + res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{}) + if res != tc.expect { + t.Errorf("got %v expected %v", res, tc.expect) + } + } +} + func TestAlertmanagerNotifier(t *testing.T) { Convey("Alertmanager notifier tests", t, func() { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index d141d6cd257..d4a9975bcba 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -67,6 +67,16 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC } // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK { + return false + } + + // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending { + return false + } + + // Do not notify when we become OK from pending if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5062828cb4f..3fd4447eefe 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStatePending, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateAlerting, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStatePending, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: true, - state: &m.AlertNotificationState{}, expect: false, }, @@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateAlerting, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - state: &m.AlertNotificationState{}, expect: true, }, @@ -132,6 +125,27 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateOK, state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()}, + expect: true, + }, + { + name: "unknown -> ok", + prevState: m.AlertStateUnknown, + newState: m.AlertStateOK, + + expect: false, + }, + { + name: "unknown -> pending", + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, + + expect: false, + }, + { + name: "unknown -> alerting", + prevState: m.AlertStateUnknown, + newState: m.AlertStateAlerting, + expect: true, }, } @@ -141,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) { State: tc.prevState, }) + if tc.state == nil { + tc.state = &m.AlertNotificationState{} + } + evalContext.Rule.State = tc.newState nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency} diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index 374b49ea957..ca5f47a322f 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -39,6 +39,39 @@ func init() { Override default channel or user, use #channel-name or @username +
+ Username + + + + Set the username for the bot's message + +
+
+ Icon emoji + + + + Provide an emoji to use as the icon for the bot's message. Overrides the icon URL + +
+
+ Icon URL + + + + Provide a URL to an image to use as the icon for the bot's message + +
Mention 0 { + filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.User.Teams)-1) + ")) OR " + params = append(params, query.User.OrgId) + for _, v := range query.User.Teams { + params = append(params, v) + } + } + filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)" + params = append(params, query.User.OrgId) + params = append(params, query.User.UserId) + params = append(params, query.User.OrgId) prefs := make([]*m.Preferences, 0) - filter := "(org_id=? AND user_id=?) OR (org_id=? AND user_id=0)" - err := x.Where(filter, query.OrgId, query.UserId, query.OrgId). - OrderBy("user_id ASC"). + err := x.Where(filter, params...). + OrderBy("user_id ASC, team_id ASC"). Find(&prefs) if err != nil { @@ -50,9 +62,8 @@ func GetPreferencesWithDefaults(query *m.GetPreferencesWithDefaultsQuery) error } func GetPreferences(query *m.GetPreferencesQuery) error { - var prefs m.Preferences - exists, err := x.Where("org_id=? AND user_id=?", query.OrgId, query.UserId).Get(&prefs) + exists, err := x.Where("org_id=? AND user_id=? AND team_id=?", query.OrgId, query.UserId, query.TeamId).Get(&prefs) if err != nil { return err @@ -71,7 +82,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { return inTransaction(func(sess *DBSession) error { var prefs m.Preferences - exists, err := sess.Where("org_id=? AND user_id=?", cmd.OrgId, cmd.UserId).Get(&prefs) + exists, err := sess.Where("org_id=? AND user_id=? AND team_id=?", cmd.OrgId, cmd.UserId, cmd.TeamId).Get(&prefs) if err != nil { return err } @@ -80,6 +91,7 @@ func SavePreferences(cmd *m.SavePreferencesCommand) error { prefs = m.Preferences{ UserId: cmd.UserId, OrgId: cmd.OrgId, + TeamId: cmd.TeamId, HomeDashboardId: cmd.HomeDashboardId, Timezone: cmd.Timezone, Theme: cmd.Theme, diff --git a/pkg/services/sqlstore/preferences_test.go b/pkg/services/sqlstore/preferences_test.go new file mode 100644 index 00000000000..f9a839bf5a7 --- /dev/null +++ b/pkg/services/sqlstore/preferences_test.go @@ -0,0 +1,91 @@ +package sqlstore + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" +) + +func TestPreferencesDataAccess(t *testing.T) { + Convey("Testing preferences data access", t, func() { + InitTestDB(t) + + Convey("GetPreferencesWithDefaults with no saved preferences should return defaults", func() { + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.Theme, ShouldEqual, setting.DefaultTheme) + So(query.Result.Timezone, ShouldEqual, "browser") + So(query.Result.HomeDashboardId, ShouldEqual, 0) + }) + + Convey("GetPreferencesWithDefaults with saved org and user home dashboard should return user home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 4) + }) + + Convey("GetPreferencesWithDefaults with saved org and other user home dashboard should return org home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 1) + }) + + Convey("GetPreferencesWithDefaults with saved org and teams home dashboard should return last team home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, Teams: []int64{2, 3}}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 3) + }) + + Convey("GetPreferencesWithDefaults with saved org and other teams home dashboard should return org home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 1) + }) + + Convey("GetPreferencesWithDefaults with saved org, teams and user home dashboard should return user home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 1, Teams: []int64{2, 3}}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 4) + }) + + Convey("GetPreferencesWithDefaults with saved org, other teams and user home dashboard should return org home dashboard", func() { + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, HomeDashboardId: 1}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 2, HomeDashboardId: 2}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, TeamId: 3, HomeDashboardId: 3}) + SavePreferences(&models.SavePreferencesCommand{OrgId: 1, UserId: 1, HomeDashboardId: 4}) + + query := &models.GetPreferencesWithDefaultsQuery{User: &models.SignedInUser{OrgId: 1, UserId: 2}} + err := GetPreferencesWithDefaults(query) + So(err, ShouldBeNil) + So(query.Result.HomeDashboardId, ShouldEqual, 1) + }) + }) +} diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index f904b44c3c8..95b53be9d4a 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -16,6 +16,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/annotations" + "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/sqlstore/migrations" "github.com/grafana/grafana/pkg/services/sqlstore/migrator" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" @@ -47,8 +48,9 @@ func init() { } type SqlStore struct { - Cfg *setting.Cfg `inject:""` - Bus bus.Bus `inject:""` + Cfg *setting.Cfg `inject:""` + Bus bus.Bus `inject:""` + CacheService *cache.CacheService `inject:""` dbCfg DatabaseConfig engine *xorm.Engine @@ -148,9 +150,11 @@ func (ss *SqlStore) Init() error { // Init repo instances annotations.SetRepository(&SqlAnnotationRepo{}) - ss.Bus.SetTransactionManager(ss) + // Register handlers + ss.addUserQueryAndCommandHandlers() + // ensure admin user if ss.skipEnsureAdmin { return nil @@ -322,6 +326,7 @@ func InitTestDB(t *testing.T) *SqlStore { sqlstore := &SqlStore{} sqlstore.skipEnsureAdmin = true sqlstore.Bus = bus.New() + sqlstore.CacheService = cache.New(5*time.Minute, 10*time.Minute) dbType := migrator.SQLITE diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 72d5654a777..99a77ecabc3 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -15,8 +15,9 @@ import ( "github.com/grafana/grafana/pkg/util" ) -func init() { - //bus.AddHandler("sql", CreateUser) +func (ss *SqlStore) addUserQueryAndCommandHandlers() { + ss.Bus.AddHandler(ss.GetSignedInUserWithCache) + bus.AddHandler("sql", GetUserById) bus.AddHandler("sql", UpdateUser) bus.AddHandler("sql", ChangeUserPassword) @@ -25,7 +26,6 @@ func init() { bus.AddHandler("sql", SetUsingOrg) bus.AddHandler("sql", UpdateUserLastSeenAt) bus.AddHandler("sql", GetUserProfile) - bus.AddHandler("sql", GetSignedInUser) bus.AddHandler("sql", SearchUsers) bus.AddHandler("sql", GetUserOrgList) bus.AddHandler("sql", DeleteUser) @@ -345,6 +345,22 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error { return err } +func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error { + cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId) + if cached, found := ss.CacheService.Get(cacheKey); found { + query.Result = cached.(*m.SignedInUser) + return nil + } + + err := GetSignedInUser(query) + if err != nil { + return err + } + + ss.CacheService.Set(cacheKey, query.Result, time.Second*5) + return nil +} + func GetSignedInUser(query *m.GetSignedInUserQuery) error { orgId := "u.org_id" if query.OrgId > 0 { @@ -389,6 +405,17 @@ func GetSignedInUser(query *m.GetSignedInUserQuery) error { user.OrgName = "Org missing" } + getTeamsByUserQuery := &m.GetTeamsByUserQuery{OrgId: user.OrgId, UserId: user.UserId} + err = GetTeamsByUser(getTeamsByUserQuery) + if err != nil { + return err + } + + user.Teams = make([]int64, len(getTeamsByUserQuery.Result)) + for i, t := range getTeamsByUserQuery.Result { + user.Teams[i] = t.Id + } + query.Result = &user return err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 16158ded002..1417392fdf8 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -13,15 +13,12 @@ import ( "regexp" "runtime" "strings" - - "gopkg.in/ini.v1" - - "github.com/go-macaron/session" - "time" + "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/util" + "gopkg.in/ini.v1" ) type Scheme string @@ -34,9 +31,15 @@ const ( ) const ( - DEV string = "development" - PROD string = "production" - TEST string = "test" + DEV = "development" + PROD = "production" + TEST = "test" + APP_NAME = "Grafana" + APP_NAME_ENTERPRISE = "Grafana Enterprise" +) + +var ( + ERR_TEMPLATE_NAME = "error" ) var ( @@ -49,10 +52,14 @@ var ( // build BuildVersion string BuildCommit string + BuildBranch string BuildStamp int64 IsEnterprise bool ApplicationName string + // packaging + Packaging = "unknown" + // Paths HomePath string PluginsPath string @@ -108,6 +115,7 @@ var ( ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string + OAuthAutoLogin bool ViewersCanEdit bool // Http auth @@ -209,12 +217,12 @@ type Cfg struct { RendererLimitAlerting int DisableBruteForceLoginProtection bool - - TempDataLifetime time.Duration - - MetricsEndpointEnabled bool - - EnableAlphaPanels bool + TempDataLifetime time.Duration + MetricsEndpointEnabled bool + MetricsEndpointBasicAuthUsername string + MetricsEndpointBasicAuthPassword string + EnableAlphaPanels bool + EnterpriseLicensePath string } type CommandLineArgs struct { @@ -533,9 +541,9 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { // Temporary keep global, to make refactor in steps Raw = cfg.Raw - ApplicationName = "Grafana" + ApplicationName = APP_NAME if IsEnterprise { - ApplicationName += " Enterprise" + ApplicationName = APP_NAME_ENTERPRISE } Env = iniFile.Section("").Key("app_mode").MustString("development") @@ -624,6 +632,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { auth := iniFile.Section("auth") DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) + OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) SignoutRedirectUrl = auth.Key("signout_redirect_url").String() // anonymous access @@ -674,6 +683,8 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { cfg.PhantomDir = filepath.Join(HomePath, "tools/phantomjs") cfg.TempDataLifetime = iniFile.Section("paths").Key("temp_data_lifetime").MustDuration(time.Second * 3600 * 24) cfg.MetricsEndpointEnabled = iniFile.Section("metrics").Key("enabled").MustBool(true) + cfg.MetricsEndpointBasicAuthUsername = iniFile.Section("metrics").Key("basic_auth_username").String() + cfg.MetricsEndpointBasicAuthPassword = iniFile.Section("metrics").Key("basic_auth_password").String() analytics := iniFile.Section("analytics") ReportingEnabled = analytics.Key("reporting_enabled").MustBool(true) @@ -715,6 +726,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { imageUploadingSection := iniFile.Section("external_image_storage") ImageUploadProvider = imageUploadingSection.Key("provider").MustString("") + + enterprise := iniFile.Section("enterprise") + cfg.EnterpriseLicensePath = enterprise.Key("license_path").MustString(filepath.Join(cfg.DataPath, "license.jwt")) + return nil } diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index e9ab08305f6..05ae2a481f2 100644 --- a/pkg/social/google_oauth.go +++ b/pkg/social/google_oauth.go @@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool { func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { var data struct { + Id string `json:"id"` Name string `json:"name"` Email string `json:"email"` } @@ -47,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi } return &BasicUserInfo{ + Id: data.Id, Name: data.Name, Email: data.Email, Login: data.Email, diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 437457df52a..8bb1ab6c928 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -126,6 +126,18 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo } eg.Go(func() error { + defer func() { + if err := recover(); err != nil { + plog.Error("Execute Query Panic", "error", err, "stack", log.Stack(1)) + if theErr, ok := err.(error); ok { + resultChan <- &tsdb.QueryResult{ + RefId: query.RefId, + Error: theErr, + } + } + } + }() + queryRes, err := e.executeQuery(ectx, query, queryContext) if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" { return err @@ -146,6 +158,17 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo for region, getMetricDataQuery := range getMetricDataQueries { q := getMetricDataQuery eg.Go(func() error { + defer func() { + if err := recover(); err != nil { + plog.Error("Execute Get Metric Data Query Panic", "error", err, "stack", log.Stack(1)) + if theErr, ok := err.(error); ok { + resultChan <- &tsdb.QueryResult{ + Error: theErr, + } + } + } + }() + queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext) if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" { return err @@ -188,8 +211,8 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatch return nil, err } - if endTime.Before(startTime) { - return nil, fmt.Errorf("Invalid time range: End time can't be before start time") + if !startTime.Before(endTime) { + return nil, fmt.Errorf("Invalid time range: Start time must be before end time") } params := &cloudwatch.GetMetricStatisticsInput{ diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go index 32b8c910f2b..3b0b073eb06 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch_test.go +++ b/pkg/tsdb/cloudwatch/cloudwatch_test.go @@ -1,9 +1,13 @@ package cloudwatch import ( + "context" "testing" "time" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/tsdb" + "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/grafana/grafana/pkg/components/null" @@ -14,6 +18,24 @@ import ( func TestCloudWatch(t *testing.T) { Convey("CloudWatch", t, func() { + Convey("executeQuery", func() { + e := &CloudWatchExecutor{ + DataSource: &models.DataSource{ + JsonData: simplejson.New(), + }, + } + + Convey("End time before start time should result in error", func() { + _, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-2h")}) + So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time") + }) + + Convey("End time equals start time should result in error", func() { + _, err := e.executeQuery(context.Background(), &CloudWatchQuery{}, &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("now-1h", "now-1h")}) + So(err.Error(), ShouldEqual, "Invalid time range: Start time must be before end time") + }) + }) + Convey("can parse cloudwatch json model", func() { json := ` { diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 718f9e0d253..76eadf1c8dc 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -46,6 +46,8 @@ func init() { "AWS/Billing": {"EstimatedCharges"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, + "AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"}, + "AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, @@ -120,6 +122,8 @@ func init() { "AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"}, "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, + "AWS/CloudHSM": {"Region", "ClusterId", "HsmId"}, + "AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, "AWS/DX": {"ConnectionId"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, diff --git a/pkg/tsdb/elasticsearch/client/search_request.go b/pkg/tsdb/elasticsearch/client/search_request.go index 4c577a2c31d..d89a98cbadb 100644 --- a/pkg/tsdb/elasticsearch/client/search_request.go +++ b/pkg/tsdb/elasticsearch/client/search_request.go @@ -112,7 +112,7 @@ func (b *SearchRequestBuilder) Query() *QueryBuilder { // Agg initiate and returns a new aggregation builder func (b *SearchRequestBuilder) Agg() AggBuilder { - aggBuilder := newAggBuilder() + aggBuilder := newAggBuilder(b.version) b.aggBuilders = append(b.aggBuilders, aggBuilder) return aggBuilder } @@ -275,11 +275,13 @@ type AggBuilder interface { type aggBuilderImpl struct { AggBuilder aggDefs []*aggDef + version int } -func newAggBuilder() *aggBuilderImpl { +func newAggBuilder(version int) *aggBuilderImpl { return &aggBuilderImpl{ aggDefs: make([]*aggDef, 0), + version: version, } } @@ -317,7 +319,7 @@ func (b *aggBuilderImpl) Histogram(key, field string, fn func(a *HistogramAgg, b }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -337,7 +339,7 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -347,6 +349,8 @@ func (b *aggBuilderImpl) DateHistogram(key, field string, fn func(a *DateHistogr return b } +const termsOrderTerm = "_term" + func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b AggBuilder)) AggBuilder { innerAgg := &TermsAggregation{ Field: field, @@ -358,11 +362,18 @@ func (b *aggBuilderImpl) Terms(key, field string, fn func(a *TermsAggregation, b }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } + if b.version >= 60 && len(innerAgg.Order) > 0 { + if orderBy, exists := innerAgg.Order[termsOrderTerm]; exists { + innerAgg.Order["_key"] = orderBy + delete(innerAgg.Order, termsOrderTerm) + } + } + b.aggDefs = append(b.aggDefs, aggDef) return b @@ -377,7 +388,7 @@ func (b *aggBuilderImpl) Filters(key string, fn func(a *FiltersAggregation, b Ag Aggregation: innerAgg, }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } @@ -398,7 +409,7 @@ func (b *aggBuilderImpl) GeoHashGrid(key, field string, fn func(a *GeoHashGridAg }) if fn != nil { - builder := newAggBuilder() + builder := newAggBuilder(b.version) aggDef.builders = append(aggDef.builders, builder) fn(innerAgg, builder) } diff --git a/pkg/tsdb/elasticsearch/time_series_query_test.go b/pkg/tsdb/elasticsearch/time_series_query_test.go index fe8ae0fa8f2..9660d70c318 100644 --- a/pkg/tsdb/elasticsearch/time_series_query_test.go +++ b/pkg/tsdb/elasticsearch/time_series_query_test.go @@ -127,6 +127,60 @@ func TestExecuteTimeSeriesQuery(t *testing.T) { So(avgAgg.Aggregation.Type, ShouldEqual, "avg") }) + Convey("With term agg and order by term", func() { + c := newFakeClient(5) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { + "type": "terms", + "field": "@host", + "id": "2", + "settings": { "size": "5", "order": "asc", "orderBy": "_term" } + }, + { "type": "date_histogram", "field": "@timestamp", "id": "3" } + ], + "metrics": [ + {"type": "count", "id": "1" }, + {"type": "avg", "field": "@value", "id": "5" } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "2") + termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) + So(termsAgg.Order["_term"], ShouldEqual, "asc") + }) + + Convey("With term agg and order by term with es6.x", func() { + c := newFakeClient(60) + _, err := executeTsdbQuery(c, `{ + "timeField": "@timestamp", + "bucketAggs": [ + { + "type": "terms", + "field": "@host", + "id": "2", + "settings": { "size": "5", "order": "asc", "orderBy": "_term" } + }, + { "type": "date_histogram", "field": "@timestamp", "id": "3" } + ], + "metrics": [ + {"type": "count", "id": "1" }, + {"type": "avg", "field": "@value", "id": "5" } + ] + }`, from, to, 15*time.Second) + So(err, ShouldBeNil) + sr := c.multisearchRequests[0].Requests[0] + + firstLevel := sr.Aggs[0] + So(firstLevel.Key, ShouldEqual, "2") + termsAgg := firstLevel.Aggregation.Aggregation.(*es.TermsAggregation) + So(termsAgg.Order["_key"], ShouldEqual, "asc") + }) + Convey("With metric percentiles", func() { c := newFakeClient(5) _, err := executeTsdbQuery(c, `{ diff --git a/pkg/tsdb/influxdb/influxdb.go b/pkg/tsdb/influxdb/influxdb.go index 6100d3b401e..ec1e9ff01bd 100644 --- a/pkg/tsdb/influxdb/influxdb.go +++ b/pkg/tsdb/influxdb/influxdb.go @@ -96,16 +96,15 @@ func (e *InfluxDBExecutor) Query(ctx context.Context, dsInfo *models.DataSource, } func (e *InfluxDBExecutor) getQuery(dsInfo *models.DataSource, queries []*tsdb.Query, context *tsdb.TsdbQuery) (*Query, error) { - for _, v := range queries { - - query, err := e.QueryParser.Parse(v.Model, dsInfo) + // The model supports multiple queries, but right now this is only used from + // alerting so we only needed to support batch executing 1 query at a time. + if len(queries) > 0 { + query, err := e.QueryParser.Parse(queries[0].Model, dsInfo) if err != nil { return nil, err } - return query, nil } - return nil, fmt.Errorf("query request contains no queries") } diff --git a/public/app/core/components/Animations/SlideDown.tsx b/public/app/core/components/Animations/SlideDown.tsx index 4d515f98f16..70dacd73849 100644 --- a/public/app/core/components/Animations/SlideDown.tsx +++ b/public/app/core/components/Animations/SlideDown.tsx @@ -1,15 +1,22 @@ -import React from 'react'; +import React from 'react'; import Transition from 'react-transition-group/Transition'; -const defaultMaxHeight = '200px'; // When animating using max-height we need to use a static value. +interface Style { + transition?: string; + overflow?: string; +} + +// When animating using max-height we need to use a static value. // If this is not enough, pass in
{title}
- + {buttonTitle} diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap index 6d47c984d5e..b85660bcc6f 100644 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap @@ -12,6 +12,7 @@ exports[`EmptyListCTA renders correctly 1`] = ` = props => { return ( - + {props.children} {props.tooltip && ( @@ -19,4 +20,3 @@ export const Label: SFC = props => { ); }; - diff --git a/public/app/core/components/Picker/SimplePicker.tsx b/public/app/core/components/Picker/SimplePicker.tsx new file mode 100644 index 00000000000..817cd14150f --- /dev/null +++ b/public/app/core/components/Picker/SimplePicker.tsx @@ -0,0 +1,49 @@ +import React, { SFC } from 'react'; +import Select from 'react-select'; +import DescriptionOption from './DescriptionOption'; +import ResetStyles from './ResetStyles'; + +interface Props { + className?: string; + defaultValue?: any; + getOptionLabel: (item: any) => string; + getOptionValue: (item: any) => string; + onSelected: (item: any) => {} | void; + options: any[]; + placeholder?: string; + width: number; + value: any; +} + +const SimplePicker: SFC = ({ + className, + defaultValue, + getOptionLabel, + getOptionValue, + onSelected, + options, + placeholder, + width, + value, +}) => { + return ( +
@@ -230,6 +236,12 @@ exports[`Render should render component 1`] = ` > Paused + diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index cb101672aa4..2ebe2b53a76 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,147 +1,159 @@
- -
-
-
- {{ctrl.error}} -
+
+
+
+ {{ctrl.error}} +
-
-
Alert Config
-
- Name - - Evaluate every - -
-
+
+
Alert Config
+
+ Name + +
+
+
+ Evaluate every + +
+
+ + + + If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications. + +
+
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+
Conditions
+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
- -
-
+
+ +
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+
+ If no data or all values are null + SET STATE TO +
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ If execution error or timeout + SET STATE TO +
+ +
+
-
- -
-
+
+ +
+
-
- Evaluating rule -
+
+ Evaluating rule +
-
- -
-
+
+ +
+
-
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
+
+
Notifications
+
+
+ Send to + +  {{nc.name}}  + + + +
+
+
+ Message + +
+
-
- -
- State history (last 50 state changes) -
+
+ +
+ State history (last 50 state changes) +
-
-
- No state changes recorded -
+
+
+ No state changes recorded +
  1. diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index 11d2aafaa7f..8e50697d33c 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -8,9 +8,9 @@ const alertQueryDef = new QueryPartDef({ { name: 'from', type: 'string', - options: ['1s', '10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'], + options: ['10s', '1m', '5m', '10m', '15m', '1h', '24h', '48h'], }, - { name: 'to', type: 'string', options: ['now'] }, + { name: 'to', type: 'string', options: ['now', 'now-1m', 'now-5m', 'now-10m', 'now-1h'] }, ], defaultParams: ['#A', '15m', 'now', 'avg'], }); @@ -99,6 +99,13 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-warning', }; } + case 'unknown': { + return { + text: 'UNKNOWN', + iconClass: 'fa fa-question', + stateClass: 'alert-state-paused', + }; + } } throw { message: 'Unknown alert state' }; diff --git a/public/app/features/all.ts b/public/app/features/all.ts index 7f6f84b7676..b1283fdfd3e 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -9,3 +9,5 @@ import './admin'; import './alerting/NotificationsEditCtrl'; import './alerting/NotificationsListCtrl'; import './manage-dashboards'; +import './teams/CreateTeamCtrl'; +import './profile/all'; diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 16c18005204..fbe85856f31 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, if (event.alertId) { const stateModel = alertDef.getStateDisplayModel(event.newState); titleStateClass = stateModel.stateClass; - title = ` ${stateModel.text}`; + title = ` ${stateModel.text}`; text = alertDef.getAlertAnnotationInfo(event); if (event.text) { text = text + '
    ' + event.text; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index ef74ca193d4..db748e639a1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -7,6 +7,7 @@ import { OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, + PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, } from 'app/core/utils/colors'; @@ -71,6 +72,11 @@ export class EventManager { position: 'BOTTOM', markerSize: 5, }, + $__pending: { + color: PENDING_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, $__editing: { color: DEFAULT_ANNOTATION_COLOR, position: 'BOTTOM', diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 8bc6e9338fc..54200234ddc 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from 'react'; import { shallow } from 'enzyme'; import { Props, ApiKeysPage } from './ApiKeysPage'; import { NavModel, ApiKey } from 'app/types'; @@ -14,6 +14,7 @@ const setup = (propOverrides?: object) => { deleteApiKey: jest.fn(), setSearchQuery: jest.fn(), addApiKey: jest.fn(), + apiKeysCount: 0, }; Object.assign(props, propOverrides); @@ -28,14 +29,19 @@ const setup = (propOverrides?: object) => { }; describe('Render', () => { - it('should render component', () => { - const { wrapper } = setup(); + it('should render API keys table if there are any keys', () => { + const { wrapper } = setup({ + apiKeys: getMultipleMockKeys(5), + apiKeysCount: 5, + }); + expect(wrapper).toMatchSnapshot(); }); - it('should render API keys table', () => { + it('should render CTA if there are no API keys', () => { const { wrapper } = setup({ - apiKeys: getMultipleMockKeys(5), + apiKeys: getMultipleMockKeys(0), + apiKeysCount: 0, hasFetched: true, }); diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 6052b0f4fc8..d2aa1f24c57 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -1,17 +1,19 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent } from 'react'; import ReactDOMServer from 'react-dom/server'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; -import { getApiKeys } from './state/selectors'; +import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import SlideDown from 'app/core/components/Animations/SlideDown'; import PageLoader from 'app/core/components/PageLoader/PageLoader'; +import SlideDown from 'app/core/components/Animations/SlideDown'; import ApiKeysAddedModal from './ApiKeysAddedModal'; import config from 'app/core/config'; import appEvents from 'app/core/app_events'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; export interface Props { navModel: NavModel; @@ -22,6 +24,7 @@ export interface Props { deleteApiKey: typeof deleteApiKey; setSearchQuery: typeof setSearchQuery; addApiKey: typeof addApiKey; + apiKeysCount: number; } export interface State { @@ -82,6 +85,7 @@ export class ApiKeysPage extends PureComponent { return { ...prevState, newApiKey: initialApiKeyState, + isAdding: false, }; }); }; @@ -101,115 +105,152 @@ export class ApiKeysPage extends PureComponent { }); }; - renderTable() { - const { apiKeys } = this.props; - - return [ -

    - Existing Keys -

    , - - - - - - - - {apiKeys.length > 0 && ( - - {apiKeys.map(key => { - return ( - - - - - - ); - })} - + renderEmptyList() { + const { isAdding } = this.state; + return ( +
    + {!isAdding && ( + )} -
    NameRole -
    {key.name}{key.role} - this.onDeleteApiKey(key)} className="btn btn-danger btn-mini"> - - -
    , - ]; + {this.renderAddApiKeyForm()} +
+ ); + } + + renderAddApiKeyForm() { + const { newApiKey, isAdding } = this.state; + + return ( + +
+ +
Add API Key
+
+
+
+ Key name + this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} + /> +
+
+ Role + + + +
+
+ +
+
+
+
+
+ ); + } + + renderApiKeyList() { + const { isAdding } = this.state; + const { apiKeys, searchQuery } = this.props; + + return ( +
+
+
+ +
+ +
+ +
+ + {this.renderAddApiKeyForm()} + +

Existing Keys

+ + + + + + + + {apiKeys.length > 0 ? ( + + {apiKeys.map(key => { + return ( + + + + + + ); + })} + + ) : null} +
NameRole +
{key.name}{key.role} + this.onDeleteApiKey(key)} /> +
+
+ ); } render() { - const { newApiKey, isAdding } = this.state; - const { hasFetched, navModel, searchQuery } = this.props; + const { hasFetched, navModel, apiKeysCount } = this.props; return (
-
-
-
- -
- -
- -
- - -
- -
Add API Key
-
-
-
- Key name - this.onApiKeyStateUpdate(evt, ApiKeyStateProps.Name)} - /> -
-
- Role - - - -
-
- -
-
-
-
-
- {hasFetched ? this.renderTable() : } -
+ {hasFetched ? ( + apiKeysCount > 0 ? ( + this.renderApiKeyList() + ) : ( + this.renderEmptyList() + ) + ) : ( + + )}
); } @@ -220,6 +261,7 @@ function mapStateToProps(state) { navModel: getNavModel(state.navIndex, 'apikeys'), apiKeys: getApiKeys(state.apiKeys), searchQuery: state.apiKeys.searchQuery, + apiKeysCount: getApiKeysCount(state.apiKeys), hasFetched: state.apiKeys.hasFetched, }; } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index b1cac8469be..7ede9618250 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -1,276 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Render should render API keys table 1`] = ` +exports[`Render should render API keys table if there are any keys 1`] = `
-
-
-
- -
-
- -
- -
- -
- Add API Key -
-
-
-
- - Key name - - -
-
- - Role - - - - -
-
- -
-
-
-
-
-

- Existing Keys -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- Name - - Role - -
- test-1 - - Viewer - - - - -
- test-2 - - Viewer - - - - -
- test-3 - - Viewer - - - - -
- test-4 - - Viewer - - - - -
- test-5 - - Viewer - - - - -
-
+
`; -exports[`Render should render component 1`] = ` +exports[`Render should render CTA if there are no API keys 1`] = `
-
-
- -
-
- -
+ @@ -406,9 +127,6 @@ exports[`Render should render component 1`] = `
-
`; diff --git a/public/app/features/api-keys/state/selectors.ts b/public/app/features/api-keys/state/selectors.ts index 8065c252e85..789237ae9a3 100644 --- a/public/app/features/api-keys/state/selectors.ts +++ b/public/app/features/api-keys/state/selectors.ts @@ -1,5 +1,7 @@ import { ApiKeysState } from 'app/types'; +export const getApiKeysCount = (state: ApiKeysState) => state.keys.length; + export const getApiKeys = (state: ApiKeysState) => { const regex = RegExp(state.searchQuery, 'i'); diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 5871a579f3c..6611a728803 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -2,13 +2,13 @@ import config from 'app/core/config'; import appEvents from 'app/core/app_events'; import coreModule from 'app/core/core_module'; +import { removePanel } from 'app/features/dashboard/utils/panel'; // Services import { AnnotationsSrv } from '../annotations/annotations_srv'; // Types import { DashboardModel } from './dashboard_model'; -import { PanelModel } from './panel_model'; export class DashboardCtrl { dashboard: DashboardModel; @@ -19,7 +19,6 @@ export class DashboardCtrl { /** @ngInject */ constructor( private $scope, - private $rootScope, private keybindingSrv, private timeSrv, private variableSrv, @@ -112,12 +111,14 @@ export class DashboardCtrl { } showJsonEditor(evt, options) { - const editScope = this.$rootScope.$new(); - editScope.object = options.object; - editScope.updateHandler = options.updateHandler; + const model = { + object: options.object, + updateHandler: options.updateHandler, + }; + this.$scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', - scope: editScope, + model: model, }); } @@ -136,34 +137,7 @@ export class DashboardCtrl { } const panelInfo = this.dashboard.getPanelInfoById(options.panelId); - this.removePanel(panelInfo.panel, true); - } - - removePanel(panel: PanelModel, ask: boolean) { - // confirm deletion - if (ask !== false) { - let text2, confirmText; - - if (panel.alert) { - text2 = 'Panel includes an alert rule, removing panel will also remove alert rule'; - confirmText = 'YES'; - } - - this.$scope.appEvent('confirm-modal', { - title: 'Remove Panel', - text: 'Are you sure you want to remove this panel?', - text2: text2, - icon: 'fa-trash', - confirmText: confirmText, - yesText: 'Remove', - onConfirm: () => { - this.removePanel(panel, false); - }, - }); - return; - } - - this.dashboard.removePanel(panel); + removePanel(this.dashboard, panelInfo.panel, true); } onDestroy() { diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 65a234a2b94..3320783ec67 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -232,11 +232,6 @@ export class DashboardModel { 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/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b1419df7376..d5695a577c5 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -77,6 +77,10 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; + // important that these happens before location redirect below + this.$rootScope.appEvent('dashboard-saved', this.dash); + this.$rootScope.appEvent('alert-success', ['Dashboard saved']); + const newUrl = locationUtil.stripBaseFromUrl(data.url); const currentPath = this.$location.path(); @@ -84,9 +88,6 @@ export class DashboardSrv { this.$location.url(newUrl).replace(); } - this.$rootScope.appEvent('dashboard-saved', this.dash); - this.$rootScope.appEvent('alert-success', ['Dashboard saved']); - return this.dash; } diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index fe55e64634f..12b4809f51a 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -83,7 +83,6 @@ export class DashboardGrid extends React.Component { 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() { @@ -176,7 +175,12 @@ export class DashboardGrid extends React.Component { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
- +
); } diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 7dd8a06996d..6853ade474b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import config from 'app/core/config'; import { PanelModel } from '../panel_model'; import { DashboardModel } from '../dashboard_model'; @@ -11,16 +11,17 @@ import { PanelChrome } from './PanelChrome'; import { PanelEditor } from './PanelEditor'; export interface Props { - panelType: string; panel: PanelModel; dashboard: DashboardModel; + isEditing: boolean; + isFullscreen: boolean; } export interface State { pluginExports: PluginExports; } -export class DashboardPanel extends React.Component { +export class DashboardPanel extends PureComponent { element: any; angularPanel: AngularComponent; pluginInfo: any; @@ -113,9 +114,8 @@ export class DashboardPanel extends React.Component { 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'; - + const containerClass = this.props.isEditing ? 'panel-editor-container' : 'panel-height-helper'; + const panelWrapperClass = this.props.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 ( diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 82b366d8126..f6fc9028726 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -5,7 +5,7 @@ import React, { ComponentClass, PureComponent } from 'react'; import { getTimeSrv } from '../time_srv'; // Components -import { PanelHeader } from './PanelHeader'; +import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; // Types @@ -21,6 +21,7 @@ export interface Props { export interface State { refreshCounter: number; + renderCounter: number; timeRange?: TimeRange; } @@ -30,11 +31,13 @@ export class PanelChrome extends PureComponent { this.state = { refreshCounter: 0, + renderCounter: 0, }; } componentDidMount() { this.props.panel.events.on('refresh', this.onRefresh); + this.props.panel.events.on('render', this.onRender); this.props.dashboard.panelInitialized(this.props.panel); } @@ -46,10 +49,19 @@ export class PanelChrome extends PureComponent { const timeSrv = getTimeSrv(); const timeRange = timeSrv.timeRange(); - this.setState({ + this.setState(prevState => ({ + ...prevState, refreshCounter: this.state.refreshCounter + 1, timeRange: timeRange, - }); + })); + }; + + onRender = () => { + console.log('onRender'); + this.setState(prevState => ({ + ...prevState, + renderCounter: this.state.renderCounter + 1, + })); }; get isVisible() { @@ -58,10 +70,12 @@ export class PanelChrome extends PureComponent { render() { const { panel, dashboard } = this.props; + const { refreshCounter, timeRange, renderCounter } = this.state; + const { datasource, targets } = panel; - const { refreshCounter, timeRange } = this.state; const PanelComponent = this.props.component; + console.log('panelChrome render'); return (
@@ -74,7 +88,16 @@ export class PanelChrome extends PureComponent { refreshCounter={refreshCounter} > {({ loading, timeSeries }) => { - return ; + console.log('panelcrome inner render'); + return ( + + ); }}
diff --git a/public/app/features/dashboard/dashgrid/PanelEditor.tsx b/public/app/features/dashboard/dashgrid/PanelEditor.tsx index 26ac8b7d2c1..371ed22fff7 100644 --- a/public/app/features/dashboard/dashgrid/PanelEditor.tsx +++ b/public/app/features/dashboard/dashgrid/PanelEditor.tsx @@ -1,12 +1,15 @@ -import React from 'react'; +import React, { PureComponent } from 'react'; import classNames from 'classnames'; + +import { QueriesTab } from './QueriesTab'; +import { VizTypePicker } from './VizTypePicker'; + +import { store } from 'app/store/configureStore'; +import { updateLocation } from 'app/core/actions'; + 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; @@ -22,7 +25,7 @@ interface PanelEditorTab { icon: string; } -export class PanelEditor extends React.Component { +export class PanelEditor extends PureComponent { tabs: PanelEditorTab[]; constructor(props) { @@ -39,16 +42,21 @@ export class PanelEditor extends React.Component { } renderPanelOptions() { - const { pluginExports } = this.props; + const { pluginExports, panel } = this.props; - if (pluginExports.PanelOptions) { - const PanelOptions = pluginExports.PanelOptions; - return ; + if (pluginExports.PanelOptionsComponent) { + const OptionsComponent = pluginExports.PanelOptionsComponent; + return ; } else { return

Visualization has no options

; } } + onPanelOptionsChanged = (options: any) => { + this.props.panel.updateOptions(options); + this.forceUpdate(); + }; + renderVizTab() { return (
@@ -70,6 +78,7 @@ export class PanelEditor extends React.Component { partial: true, }) ); + this.forceUpdate(); }; render() { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader.tsx deleted file mode 100644 index 12d5cd37253..00000000000 --- a/public/app/features/dashboard/dashgrid/PanelHeader.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx new file mode 100644 index 00000000000..ab3081ba21e --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -0,0 +1,51 @@ +import React, { PureComponent } from 'react'; +import classNames from 'classnames'; + +import { PanelHeaderMenu } from './PanelHeaderMenu'; + +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; + +export interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeader extends PureComponent { + render() { + const isFullscreen = false; + const isLoading = false; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); + const { panel, dashboard } = this.props; + + return ( +
+ + + + + + {isLoading && ( + + + + )} + +
+
+ + + {panel.title} + + + + + + 4m + +
+
+
+ ); + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx new file mode 100644 index 00000000000..8eb6b31bf27 --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -0,0 +1,40 @@ +import React, { PureComponent } from 'react'; +import { DashboardModel } from 'app/features/dashboard/dashboard_model'; +import { PanelModel } from 'app/features/dashboard/panel_model'; +import { PanelHeaderMenuItem } from './PanelHeaderMenuItem'; +import { getPanelMenu } from 'app/features/dashboard/utils/getPanelMenu'; +import { PanelMenuItem } from 'app/types/panel'; + +export interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +export class PanelHeaderMenu extends PureComponent { + renderItems = (menu: PanelMenuItem[], isSubMenu = false) => { + return ( +
    + {menu.map((menuItem, idx: number) => { + return ( + + {menuItem.subMenu && this.renderItems(menuItem.subMenu, true)} + + ); + })} +
+ ); + }; + + render() { + const { dashboard, panel } = this.props; + const menu = getPanelMenu(dashboard, panel); + return
{this.renderItems(menu)}
; + } +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx new file mode 100644 index 00000000000..92a64a2f24d --- /dev/null +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenuItem.tsx @@ -0,0 +1,23 @@ +import React, { SFC } from 'react'; +import { PanelMenuItem } from 'app/types/panel'; + +interface Props { + children: any; +} + +export const PanelHeaderMenuItem: SFC = props => { + const isSubMenu = props.type === 'submenu'; + const isDivider = props.type === 'divider'; + return isDivider ? ( +
  • + ) : ( +
  • + + {props.iconClassName && } + {props.text} + {props.shortcut && {props.shortcut}} + + {props.children} +
  • + ); +}; diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html index 0598c612fd6..3c14c4b184d 100644 --- a/public/app/features/dashboard/export/export_modal.html +++ b/public/app/features/dashboard/export/export_modal.html @@ -1,25 +1,21 @@ - - - - - -