diff --git a/.circleci/config.yml b/.circleci/config.yml index 23414436bf8f4..4c49d051178ea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -198,6 +198,117 @@ jobs: paths: - dist/* + build-fast-backend: + docker: + - image: grafana/build-container:1.2.6 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: build grafana backend + command: './scripts/build/build.sh --fast --backend-only' + - persist_to_workspace: + root: . + paths: + - bin/* + + build-fast-frontend: + docker: + - image: grafana/build-container:1.2.6 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - restore_cache: + key: frontend-dependency-cache-{{ checksum "yarn.lock" }} + - run: + name: build grafana frontend + command: './scripts/build/build.sh --fast --frontend-only' + - save_cache: + key: frontend-dependency-cache-{{ checksum "yarn.lock" }} + paths: + - node_modules + - persist_to_workspace: + root: . + paths: + - public/build/* + - tools/phantomjs/* + + build-fast-package: + docker: + - image: grafana/build-container:1.2.6 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - attach_workspace: + at: . + - restore_cache: + key: frontend-dependency-cache-{{ checksum "yarn.lock" }} + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: package grafana + command: './scripts/build/build.sh --fast --package-only' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - run: + name: Test Grafana.com release publisher + command: 'cd scripts/build/release_publisher && go test .' + - persist_to_workspace: + root: /go/src/github.com/grafana/grafana + paths: + - dist/* + + build-fast-save: + docker: + - image: grafana/build-container:1.2.6 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - attach_workspace: + at: . + - restore_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + - run: + name: debug cache + command: 'ls -al /go/src/github.com/grafana/grafana/node_modules' + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: build grafana backend + command: './scripts/build/build.sh --fast --backend-only' + - run: + name: build grafana frontend + command: './scripts/build/build.sh --fast --frontend-only' + - save_cache: + key: dependency-cache-{{ checksum "yarn.lock" }} + paths: + - /go/src/github.com/grafana/grafana/node_modules + - run: + name: package grafana + command: './scripts/build/build.sh --fast --package-only' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - run: + name: Test Grafana.com release publisher + command: 'cd scripts/build/release_publisher && go test .' + - persist_to_workspace: + root: . + paths: + - dist/* + grafana-docker-master: machine: image: circleci/classic:201808-01 @@ -224,7 +335,7 @@ jobs: - run: docker info - run: docker run --privileged linuxkit/binfmt:v0.6 - run: cp dist/grafana-latest.linux-*.tar.gz packaging/docker - - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}" + - run: cd packaging/docker && ./build.sh --fast "${CIRCLE_SHA1}" grafana-docker-release: machine: @@ -556,8 +667,15 @@ workflows: build-branches-and-prs: jobs: - - build: + - build-fast-backend: + filters: *filter-not-release-or-master + - build-fast-frontend: filters: *filter-not-release-or-master + - build-fast-package: + filters: *filter-not-release-or-master + requires: + - build-fast-backend + - build-fast-frontend - codespell: filters: *filter-not-release-or-master - backend-lint: @@ -574,7 +692,7 @@ workflows: filters: *filter-not-release-or-master - grafana-docker-pr: requires: - - build + - build-fast-package - test-backend - test-frontend - codespell @@ -585,7 +703,7 @@ workflows: filters: *filter-not-release-or-master - store-build-artifacts: requires: - - build + - build-fast-package - test-backend - test-frontend - codespell diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e8fec5ac833d..752c8f8caf959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # 6.2.0 (unreleased) +# 6.1.6 (2019-04-29) +### Features / Enhancements +* **Security**: Bump jQuery to 3.4.0 . [#16761](https://github.com/grafana/grafana/pull/16761), [@dprokop](https://github.com/dprokop) + +### Bug Fixes +* **Playlist**: Fix loading dashboards by tag. [#16727](https://github.com/grafana/grafana/pull/16727), [@marefr](https://github.com/marefr) + +# 6.1.5 (2019-04-29) + +* **Security**: Urgent security patch release. Please read more in our [blog](https://grafana.com/blog/2019/04/29/grafana-5.4.4-and-6.1.6-released-with-important-security-fix/) + # 6.1.4 (2019-04-16) ### Bug Fixes diff --git a/Dockerfile b/Dockerfile index e1ae6a7508f3d..9e5551bd4a4a0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,14 +3,10 @@ FROM golang:1.12.4 WORKDIR $GOPATH/src/github.com/grafana/grafana -COPY Gopkg.toml Gopkg.lock ./ +COPY go.mod go.sum ./ COPY vendor vendor -ARG DEP_ENSURE="" -RUN if [ ! -z "${DEP_ENSURE}" ]; then \ - go get -u github.com/golang/dep/cmd/dep && \ - dep ensure --vendor-only; \ - fi +RUN go mod verify COPY pkg pkg COPY build.go build.go diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index fe3d682b59a40..0000000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,937 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[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:ebe102b61c1615d2954734e3cfe1b6b06a5088c25a41055b38661d41ad7b8f27" - name = "github.com/aws/aws-sdk-go" - packages = [ - "aws", - "aws/awserr", - "aws/awsutil", - "aws/client", - "aws/client/metadata", - "aws/corehandlers", - "aws/credentials", - "aws/credentials/ec2rolecreds", - "aws/credentials/endpointcreds", - "aws/credentials/processcreds", - "aws/credentials/stscreds", - "aws/csm", - "aws/defaults", - "aws/ec2metadata", - "aws/endpoints", - "aws/request", - "aws/session", - "aws/signer/v4", - "internal/ini", - "internal/s3err", - "internal/sdkio", - "internal/sdkrand", - "internal/sdkuri", - "internal/shareddefaults", - "private/protocol", - "private/protocol/ec2query", - "private/protocol/eventstream", - "private/protocol/eventstream/eventstreamapi", - "private/protocol/json/jsonutil", - "private/protocol/jsonrpc", - "private/protocol/query", - "private/protocol/query/queryutil", - "private/protocol/rest", - "private/protocol/restxml", - "private/protocol/xml/xmlutil", - "service/cloudwatch", - "service/ec2", - "service/ec2/ec2iface", - "service/resourcegroupstaggingapi", - "service/resourcegroupstaggingapi/resourcegroupstaggingapiiface", - "service/s3", - "service/sts", - ] - pruneopts = "NUT" - revision = "62936e15518acb527a1a9cb4a39d96d94d0fd9a2" - version = "v1.16.15" - -[[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", - ] - 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]] - 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:f43e840e8efb7b5047c1f60057702550fcdefd2b29e3a73ccea25e27d2e83fda" - name = "github.com/go-macaron/session" - packages = ["."] - 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:06d21295033f211588d0ad7ff391cc1b27e72b60cb6d4b7db0d70cffae4cf228" - name = "github.com/go-xorm/builder" - packages = ["."] - pruneopts = "NUT" - revision = "1d658d7596c25394aab557ef5b50ef35bf706384" - version = "v0.3.4" - -[[projects]] - digest = "1:b26928aab0fff92592e8728c5bc9d6e404fa2017d6a8e841ae5e60a42237f6fc" - name = "github.com/go-xorm/core" - packages = ["."] - pruneopts = "NUT" - revision = "ccc80c1adf1f6172bbc548877f50a1163041a40a" - version = "v0.6.2" - -[[projects]] - digest = "1:407316703b32d68ccf5d39bdae57d411b6954e253e07d0fff0988a3f39861f2f" - name = "github.com/go-xorm/xorm" - packages = ["."] - pruneopts = "NUT" - revision = "1f39c590c64924f358c0d89016ac9b2bb84e9125" - version = "v0.7.1" - -[[projects]] - branch = "master" - digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0" - name = "github.com/golang/protobuf" - packages = [ - "proto", - "ptypes", - "ptypes/any", - "ptypes/duration", - "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", - ] - 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:1f2aebae7e7c856562355ec0198d8ca2fa222fb05e5b1b66632a1fce39631885" - name = "github.com/jmespath/go-jmespath" - packages = ["."] - pruneopts = "NUT" - revision = "c2b33e84" - -[[projects]] - digest = "1:395b1480ae42c3fec6fff19823e66e173819f85826811387f9045c88515a7f0f" - name = "github.com/jtolds/gls" - packages = ["."] - pruneopts = "NUT" - revision = "b4936e06046bbecbb94cae9c18127ebe510a2cb9" - -[[projects]] - digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3" - name = "github.com/klauspost/compress" - packages = [ - "flate", - "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", - ] - 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", - ] - 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", - ] - 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", - ] - pruneopts = "NUT" - revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c" - -[[projects]] - branch = "master" - digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349" - name = "github.com/prometheus/procfs" - packages = [ - ".", - "internal/util", - "nfs", - "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:a0509115762ee481fd95b60521b4dcc5ad226c54b741a4924f4f28c0cc6aabc8" - name = "github.com/smartystreets/assertions" - packages = [ - ".", - "internal/go-diff/diffmatchpatch", - "internal/go-render/render", - "internal/oglematchers", - ] - pruneopts = "NUT" - revision = "f487f9de1cd36ebab28235b9373028812fb47cbd" - -[[projects]] - digest = "1:4dccd132a83155851c5e9faaa134ee3a931965c666b6b3c076e238fe9b3577a4" - name = "github.com/smartystreets/goconvey" - packages = [ - "convey", - "convey/gotest", - "convey/reporting", - ] - pruneopts = "NUT" - revision = "68dc04aab96ae4326137d6b77330c224063a927e" - -[[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 = [ - ".", - "config", - "internal/baggage", - "internal/baggage/remote", - "internal/spanlog", - "internal/throttler", - "internal/throttler/remote", - "log", - "rpcmetrics", - "thrift", - "thrift-gen/agent", - "thrift-gen/baggage", - "thrift-gen/jaeger", - "thrift-gen/sampling", - "thrift-gen/zipkincore", - "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", - ] - 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", - ] - pruneopts = "NUT" - revision = "1a580b3eff7814fc9b40602fd35256c63b50f491" - -[[projects]] - branch = "master" - digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5" - name = "golang.org/x/net" - packages = [ - "context", - "context/ctxhttp", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "trace", - ] - pruneopts = "NUT" - revision = "2491c5de3490fced2f6cff376127c667efeed857" - -[[projects]] - branch = "master" - digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44" - name = "golang.org/x/oauth2" - packages = [ - ".", - "google", - "internal", - "jws", - "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", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "NUT" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b" - name = "google.golang.org/appengine" - packages = [ - ".", - "cloudsql", - "internal", - "internal/app_identity", - "internal/base", - "internal/datastore", - "internal/log", - "internal/modules", - "internal/remote_api", - "internal/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 = [ - ".", - "balancer", - "balancer/base", - "balancer/roundrobin", - "codes", - "connectivity", - "credentials", - "encoding", - "encoding/proto", - "grpclb/grpc_lb_v1/messages", - "grpclog", - "health", - "health/grpc_health_v1", - "internal", - "keepalive", - "metadata", - "naming", - "peer", - "resolver", - "resolver/dns", - "resolver/passthrough", - "stats", - "status", - "tap", - "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:c847b7fea4c7e6db5281a37dffc4620cb78c1227403a79e5aa290db517657ac1" - name = "gopkg.in/ldap.v3" - packages = ["."] - pruneopts = "NUT" - revision = "5c2c0f997205c29de14cb6c35996370c2c5dfab1" - version = "v3" - -[[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", - ] - pruneopts = "NUT" - revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8" - version = "v2.1.9" - -[[projects]] - branch = "v2" - digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "NUT" - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - 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/resourcegroupstaggingapi", - "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface", - "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/bradfitz/gomemcache/memcache", - "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-macaron/binding", - "github.com/go-macaron/gzip", - "github.com/go-macaron/session", - "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/jtolds/gls", - "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/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/assertions", - "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/ldap.v3", - "gopkg.in/macaron.v1", - "gopkg.in/mail.v2", - "gopkg.in/redis.v2", - "gopkg.in/square/go-jose.v2", - "gopkg.in/yaml.v2", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/UPGRADING_DEPENDENCIES.md b/UPGRADING_DEPENDENCIES.md index 7d489556981a3..16e6ce9ad4459 100644 --- a/UPGRADING_DEPENDENCIES.md +++ b/UPGRADING_DEPENDENCIES.md @@ -18,10 +18,21 @@ Upgrading Go or Node.js requires making changes in many different files. See bel ## Go Dependencies -Updated using `dep`. +The Grafana project uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies on external packages. This requires a working Go environment with version 1.11 or greater installed. -- `Gopkg.toml` -- `Gopkg.lock` +All dependencies are vendored in the `vendor/` directory. + +To add or update a new dependency, use the `go get` command: + +```bash +# Pick the latest tagged release. +go get example.com/some/module/pkg + +# Pick a specific version. +go get example.com/some/module/pkg@vX.Y.Z +``` + +Tidy up the `go.mod` and `go.sum` files and copy the new/updated dependency to the `vendor/` directory: ## Node.js Dependencies diff --git a/docs/sources/http_api/folder_dashboard_search.md b/docs/sources/http_api/folder_dashboard_search.md index 0a736a277b78d..cee7d335cc32a 100644 --- a/docs/sources/http_api/folder_dashboard_search.md +++ b/docs/sources/http_api/folder_dashboard_search.md @@ -24,7 +24,7 @@ Query parameters: - **folderIds** – List of folder id's to search in for dashboards - **starred** – Flag indicating if only starred Dashboards should be returned - **limit** – Limit the number of returned results (max 5000) -- **page** – Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. +- **page** – Use this parameter to access hits beyond limit. Numbering starts at 1. limit param acts as page size. Only available in Grafana v6.2+. **Example request for retrieving folders and dashboards of the general folder**: diff --git a/latest.json b/latest.json index 42613210e0f1f..47c25593c18d6 100644 --- a/latest.json +++ b/latest.json @@ -1,4 +1,4 @@ { - "stable": "6.1.3", - "testing": "6.1.3" + "stable": "6.1.6", + "testing": "6.1.6" } diff --git a/packages/grafana-ui/src/components/FormField/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx index 310af17c5dda3..600fc4038b700 100644 --- a/packages/grafana-ui/src/components/FormField/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -1,8 +1,10 @@ import React, { InputHTMLAttributes, FunctionComponent } from 'react'; import { FormLabel } from '../FormLabel/FormLabel'; +import { PopperContent } from '../Tooltip/PopperController'; export interface Props extends InputHTMLAttributes { label: string; + tooltip?: PopperContent; labelWidth?: number; inputWidth?: number; inputEl?: React.ReactNode; @@ -17,10 +19,19 @@ const defaultProps = { * Default form field including label used in Grafana UI. Default input element is simple . You can also pass * custom inputEl if required in which case inputWidth and inputProps are ignored. */ -export const FormField: FunctionComponent = ({ label, labelWidth, inputWidth, inputEl, ...inputProps }) => { +export const FormField: FunctionComponent = ({ + label, + tooltip, + labelWidth, + inputWidth, + inputEl, + ...inputProps +}) => { return (
- {label} + + {label} + {inputEl || }
); diff --git a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index eceaa0648083c..5e54cb2de4393 100644 --- a/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -1,6 +1,7 @@ import React, { FunctionComponent, ReactNode } from 'react'; import classNames from 'classnames'; import { Tooltip } from '../Tooltip/Tooltip'; +import { PopperContent } from '../Tooltip/PopperController'; interface Props { children: ReactNode; @@ -8,7 +9,7 @@ interface Props { htmlFor?: string; isFocused?: boolean; isInvalid?: boolean; - tooltip?: string; + tooltip?: PopperContent; width?: number; } diff --git a/packages/grafana-ui/src/components/Table/Table.story.tsx b/packages/grafana-ui/src/components/Table/Table.story.tsx index 71206af2b0ab9..1fea903adbd9d 100644 --- a/packages/grafana-ui/src/components/Table/Table.story.tsx +++ b/packages/grafana-ui/src/components/Table/Table.story.tsx @@ -43,7 +43,7 @@ export function makeDummyTable(columnCount: number, rowCount: number): SeriesDat }; } -storiesOf('Alpha/Table', module) +storiesOf('UI/Table', module) .add('Basic Table', () => { // NOTE: This example does not seem to survice rotate & // Changing fixed headers... but the next one does? @@ -56,7 +56,7 @@ storiesOf('Alpha/Table', module) return withFullSizeStory(Table, { styles: [], - data: simpleTable, + data: { ...simpleTable }, replaceVariables, showHeader, fixedHeader, diff --git a/packages/grafana-ui/src/components/Table/Table.tsx b/packages/grafana-ui/src/components/Table/Table.tsx index d2a65bdf9b060..3d759ec1ae521 100644 --- a/packages/grafana-ui/src/components/Table/Table.tsx +++ b/packages/grafana-ui/src/components/Table/Table.tsx @@ -61,6 +61,7 @@ export class Table extends Component { renderer: ColumnRenderInfo[]; measurer: CellMeasurerCache; scrollToTop = false; + rotateWidth = 100; static defaultProps = { showHeader: true, @@ -85,7 +86,7 @@ export class Table extends Component { } componentDidUpdate(prevProps: Props, prevState: State) { - const { data, styles, showHeader } = this.props; + const { data, styles, showHeader, rotate } = this.props; const { sortBy, sortDirection } = this.state; const dataChanged = data !== prevProps.data; const configsChanged = @@ -105,6 +106,11 @@ export class Table extends Component { this.renderer = this.initColumns(this.props); } + if (dataChanged || rotate !== prevProps.rotate) { + const { width, minColumnWidth } = this.props; + this.rotateWidth = Math.max(width / data.rows.length, minColumnWidth); + } + // Update the data when data or sort changes if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) { this.scrollToTop = true; @@ -115,6 +121,10 @@ export class Table extends Component { /** Given the configuration, setup how each column gets rendered */ initColumns(props: Props): ColumnRenderInfo[] { const { styles, data, width, minColumnWidth } = props; + if (!data || !data.fields || !data.fields.length || !styles) { + return []; + } + const columnWidth = Math.max(width / data.fields.length, minColumnWidth); return data.fields.map((col, index) => { @@ -235,12 +245,18 @@ export class Table extends Component { }; getColumnWidth = (col: Index): number => { + if (this.props.rotate) { + return this.rotateWidth; // fixed for now + } return this.renderer[col.index].width; }; render() { const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props; const { data } = this.state; + if (!data || !data.fields || !data.fields.length) { + return Missing Fields; // nothing + } let columnCount = data.fields.length; let rowCount = data.rows.length + (showHeader ? 1 : 0); diff --git a/packages/grafana-ui/src/components/Table/examples.ts b/packages/grafana-ui/src/components/Table/examples.ts index 15b430d892013..efeac94969d1e 100644 --- a/packages/grafana-ui/src/components/Table/examples.ts +++ b/packages/grafana-ui/src/components/Table/examples.ts @@ -162,6 +162,6 @@ export const migratedTestStyles: ColumnStyle[] = [ export const simpleTable = { type: 'table', - columns: [{ name: 'First' }, { name: 'Second' }, { name: 'Third' }], + fields: [{ name: 'First' }, { name: 'Second' }, { name: 'Third' }], rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]], }; diff --git a/packages/grafana-ui/src/types/data.ts b/packages/grafana-ui/src/types/data.ts index fbde1b14fc122..12570519b0453 100644 --- a/packages/grafana-ui/src/types/data.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -19,6 +19,12 @@ export interface QueryResultMeta { // Match the result to the query requestId?: string; + + // Used in Explore for highlighting + search?: string; + + // Used in Explore to show limit applied to search result + limit?: number; } export interface QueryResultBase { diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index 8cd1e65289699..015d7cd10a1a7 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -1,6 +1,6 @@ import { ComponentClass } from 'react'; import { TimeRange } from './time'; -import { PluginMeta } from './plugin'; +import { PluginMeta, GrafanaPlugin } from './plugin'; import { TableData, TimeSeries, SeriesData, LoadingState } from './data'; import { PanelData } from './panel'; @@ -8,11 +8,14 @@ export interface DataSourcePluginOptionsEditorProps { options: TOptions; onOptionsChange: (options: TOptions) => void; } -export class DataSourcePlugin { +export class DataSourcePlugin extends GrafanaPlugin< + DataSourcePluginMeta +> { DataSourceClass: DataSourceConstructor; components: DataSourcePluginComponents; constructor(DataSourceClass: DataSourceConstructor) { + super(); this.DataSourceClass = DataSourceClass; this.components = {}; } @@ -68,6 +71,24 @@ export class DataSourcePlugin { QueryCtrl?: any; ConfigCtrl?: any; @@ -137,7 +158,11 @@ export interface DataSourceApi { * we attach the components to this instance for easy access */ components?: DataSourcePluginComponents; - meta?: PluginMeta; + + /** + * static information about the datasource + */ + meta?: DataSourcePluginMeta; } export interface ExploreDataSourceApi extends DataSourceApi { @@ -340,7 +365,7 @@ export interface DataSourceInstanceSettings { id: number; type: string; name: string; - meta: PluginMeta; + meta: DataSourcePluginMeta; url?: string; jsonData: { [str: string]: any }; username?: string; @@ -359,6 +384,6 @@ export interface DataSourceInstanceSettings { export interface DataSourceSelectItem { name: string; value: string | null; - meta: PluginMeta; + meta: DataSourcePluginMeta; sort: string; } diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index bf3b5e2995fdc..2980686cd3b27 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -6,6 +6,7 @@ export * from './datasource'; export * from './theme'; export * from './graph'; export * from './threshold'; +export * from './navModel'; export * from './input'; export * from './logs'; export * from './displayValue'; diff --git a/public/app/types/navModel.ts b/packages/grafana-ui/src/types/navModel.ts similarity index 68% rename from public/app/types/navModel.ts rename to packages/grafana-ui/src/types/navModel.ts index aae4a030cb43c..2f1018eed9cee 100644 --- a/public/app/types/navModel.ts +++ b/packages/grafana-ui/src/types/navModel.ts @@ -1,15 +1,15 @@ export interface NavModelItem { text: string; - url: string; + url?: string; subTitle?: string; icon?: string; img?: string; - id: string; + id?: string; active?: boolean; hideFromTabs?: boolean; divider?: boolean; children?: NavModelItem[]; - breadcrumbs?: Array<{ title: string; url: string }>; + breadcrumbs?: NavModelBreadcrumb[]; target?: string; parentItem?: NavModelItem; } @@ -17,6 +17,12 @@ export interface NavModelItem { export interface NavModel { main: NavModelItem; node: NavModelItem; + breadcrumbs?: NavModelItem[]; +} + +export interface NavModelBreadcrumb { + title: string; + url?: string; } export type NavIndex = { [s: string]: NavModelItem }; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index 5354df29d1de1..ea28ca2fb47da 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -2,9 +2,25 @@ import { ComponentClass, ComponentType } from 'react'; import { LoadingState, SeriesData } from './data'; import { TimeRange } from './time'; import { ScopedVars, DataQueryRequest, DataQueryError, LegacyResponseData } from './datasource'; +import { PluginMeta, GrafanaPlugin } from './plugin'; export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string; +export interface PanelPluginMeta extends PluginMeta { + hideFromList?: boolean; + sort: number; + + // if length>0 the query tab will show up + // Before 6.2 this could be table and/or series, but 6.2+ supports both transparently + // so it will be deprecated soon + dataFormats?: PanelDataFormat[]; +} + +export enum PanelDataFormat { + Table = 'table', + TimeSeries = 'time_series', +} + export interface PanelData { state: LoadingState; series: SeriesData[]; @@ -53,14 +69,20 @@ export type PanelTypeChangedHandler = ( prevOptions: any ) => Partial; -export class PanelPlugin { +export class PanelPlugin extends GrafanaPlugin { panel: ComponentType>; editor?: ComponentClass>; defaults?: TOptions; onPanelMigration?: PanelMigrationHandler; onPanelTypeChanged?: PanelTypeChangedHandler; + /** + * Legacy angular ctrl. If this exists it will be used instead of the panel + */ + angularPanelCtrl?: any; + constructor(panel: ComponentType>) { + super(); this.panel = panel; } @@ -95,16 +117,6 @@ export class PanelPlugin { } } -export class AngularPanelPlugin { - components: { - PanelCtrl: any; - }; - - constructor(PanelCtrl: any) { - this.components = { PanelCtrl: PanelCtrl }; - } -} - export interface PanelSize { width: number; height: number; diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index f3ab8ba5da2b2..af5382426ba5d 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -24,23 +24,10 @@ export interface PluginMeta { // Filled in by the backend jsonData?: { [str: string]: any }; enabled?: boolean; - - // Datasource-specific - builtIn?: boolean; - metrics?: boolean; - tables?: boolean; - logs?: boolean; - explore?: boolean; - annotations?: boolean; - mixed?: boolean; - hasQueryHelp?: boolean; - queryOptions?: PluginMetaQueryOptions; -} - -interface PluginMetaQueryOptions { - cacheTimeout?: boolean; - maxDataPoints?: boolean; - minInterval?: boolean; + defaultNavUrl?: string; + hasUpdate?: boolean; + latestVersion?: string; + pinned?: boolean; } export enum PluginIncludeType { @@ -82,16 +69,20 @@ export interface PluginMetaInfo { version: string; } -export class AppPlugin { - meta: PluginMeta; +export class GrafanaPlugin { + // Meta is filled in by the plugin loading system + meta?: T; + + // Soon this will also include common config options +} +export class AppPlugin extends GrafanaPlugin { angular?: { ConfigCtrl?: any; pages: { [component: string]: any }; }; - constructor(meta: PluginMeta, pluginExports: any) { - this.meta = meta; + setComponentsFromLegacyExports(pluginExports: any) { const legacy = { ConfigCtrl: undefined, pages: {} as any, @@ -102,7 +93,8 @@ export class AppPlugin { this.angular = legacy; } - if (meta.includes) { + const { meta } = this; + if (meta && meta.includes) { for (const include of meta.includes) { const { type, component } = include; if (type === PluginIncludeType.page && component) { diff --git a/packages/grafana-ui/src/types/time.ts b/packages/grafana-ui/src/types/time.ts index 2bc22485b5cdc..d6c524ac19a9d 100644 --- a/packages/grafana-ui/src/types/time.ts +++ b/packages/grafana-ui/src/types/time.ts @@ -11,11 +11,30 @@ export interface TimeRange { raw: RawTimeRange; } +export interface AbsoluteTimeRange { + from: number; + to: number; +} + export interface IntervalValues { interval: string; // 10s,5m intervalMs: number; } +export interface TimeZone { + raw: string; + isUtc: boolean; +} + +export const parseTimeZone = (raw: string): TimeZone => { + return { + raw, + isUtc: raw === 'utc', + }; +}; + +export const DefaultTimeZone = parseTimeZone('browser'); + export interface TimeOption { from: string; to: string; diff --git a/packages/grafana-ui/src/utils/fieldCache.test.ts b/packages/grafana-ui/src/utils/fieldCache.test.ts new file mode 100644 index 0000000000000..9f86b6df3e0ac --- /dev/null +++ b/packages/grafana-ui/src/utils/fieldCache.test.ts @@ -0,0 +1,71 @@ +import { FieldType } from '../types/index'; +import { FieldCache } from './fieldCache'; + +describe('FieldCache', () => { + it('when creating a new FieldCache from fields should be able to query cache', () => { + const fields = [ + { name: 'time', type: FieldType.time }, + { name: 'string', type: FieldType.string }, + { name: 'number', type: FieldType.number }, + { name: 'boolean', type: FieldType.boolean }, + { name: 'other', type: FieldType.other }, + { name: 'undefined' }, + ]; + const fieldCache = new FieldCache(fields); + const allFields = fieldCache.getFields(); + expect(allFields).toHaveLength(6); + + const expectedFields = [ + { ...fields[0], index: 0 }, + { ...fields[1], index: 1 }, + { ...fields[2], index: 2 }, + { ...fields[3], index: 3 }, + { ...fields[4], index: 4 }, + { ...fields[5], type: FieldType.other, index: 5 }, + ]; + + expect(allFields).toMatchObject(expectedFields); + + expect(fieldCache.hasFieldOfType(FieldType.time)).toBeTruthy(); + expect(fieldCache.hasFieldOfType(FieldType.string)).toBeTruthy(); + expect(fieldCache.hasFieldOfType(FieldType.number)).toBeTruthy(); + expect(fieldCache.hasFieldOfType(FieldType.boolean)).toBeTruthy(); + expect(fieldCache.hasFieldOfType(FieldType.other)).toBeTruthy(); + + expect(fieldCache.getFields(FieldType.time)).toMatchObject([expectedFields[0]]); + expect(fieldCache.getFields(FieldType.string)).toMatchObject([expectedFields[1]]); + expect(fieldCache.getFields(FieldType.number)).toMatchObject([expectedFields[2]]); + expect(fieldCache.getFields(FieldType.boolean)).toMatchObject([expectedFields[3]]); + expect(fieldCache.getFields(FieldType.other)).toMatchObject([expectedFields[4], expectedFields[5]]); + + expect(fieldCache.getFieldByIndex(0)).toMatchObject(expectedFields[0]); + expect(fieldCache.getFieldByIndex(1)).toMatchObject(expectedFields[1]); + expect(fieldCache.getFieldByIndex(2)).toMatchObject(expectedFields[2]); + expect(fieldCache.getFieldByIndex(3)).toMatchObject(expectedFields[3]); + expect(fieldCache.getFieldByIndex(4)).toMatchObject(expectedFields[4]); + expect(fieldCache.getFieldByIndex(5)).toMatchObject(expectedFields[5]); + expect(fieldCache.getFieldByIndex(6)).toBeNull(); + + expect(fieldCache.getFirstFieldOfType(FieldType.time)).toMatchObject(expectedFields[0]); + expect(fieldCache.getFirstFieldOfType(FieldType.string)).toMatchObject(expectedFields[1]); + expect(fieldCache.getFirstFieldOfType(FieldType.number)).toMatchObject(expectedFields[2]); + expect(fieldCache.getFirstFieldOfType(FieldType.boolean)).toMatchObject(expectedFields[3]); + expect(fieldCache.getFirstFieldOfType(FieldType.other)).toMatchObject(expectedFields[4]); + + expect(fieldCache.hasFieldNamed('tim')).toBeFalsy(); + expect(fieldCache.hasFieldNamed('time')).toBeTruthy(); + expect(fieldCache.hasFieldNamed('string')).toBeTruthy(); + expect(fieldCache.hasFieldNamed('number')).toBeTruthy(); + expect(fieldCache.hasFieldNamed('boolean')).toBeTruthy(); + expect(fieldCache.hasFieldNamed('other')).toBeTruthy(); + expect(fieldCache.hasFieldNamed('undefined')).toBeTruthy(); + + expect(fieldCache.getFieldByName('time')).toMatchObject(expectedFields[0]); + expect(fieldCache.getFieldByName('string')).toMatchObject(expectedFields[1]); + expect(fieldCache.getFieldByName('number')).toMatchObject(expectedFields[2]); + expect(fieldCache.getFieldByName('boolean')).toMatchObject(expectedFields[3]); + expect(fieldCache.getFieldByName('other')).toMatchObject(expectedFields[4]); + expect(fieldCache.getFieldByName('undefined')).toMatchObject(expectedFields[5]); + expect(fieldCache.getFieldByName('null')).toBeNull(); + }); +}); diff --git a/packages/grafana-ui/src/utils/fieldCache.ts b/packages/grafana-ui/src/utils/fieldCache.ts new file mode 100644 index 0000000000000..b430e6a3f2195 --- /dev/null +++ b/packages/grafana-ui/src/utils/fieldCache.ts @@ -0,0 +1,76 @@ +import { Field, FieldType } from '../types/index'; + +export interface IndexedField extends Field { + index: number; +} + +export class FieldCache { + private fields: Field[]; + private fieldIndexByName: { [key: string]: number }; + private fieldIndexByType: { [key: string]: number[] }; + + constructor(fields?: Field[]) { + this.fields = []; + this.fieldIndexByName = {}; + this.fieldIndexByType = {}; + this.fieldIndexByType[FieldType.time] = []; + this.fieldIndexByType[FieldType.string] = []; + this.fieldIndexByType[FieldType.number] = []; + this.fieldIndexByType[FieldType.boolean] = []; + this.fieldIndexByType[FieldType.other] = []; + + if (fields) { + for (let n = 0; n < fields.length; n++) { + const field = fields[n]; + this.addField(field); + } + } + } + + addField(field: Field) { + this.fields.push({ + type: FieldType.other, + ...field, + }); + const index = this.fields.length - 1; + this.fieldIndexByName[field.name] = index; + this.fieldIndexByType[field.type || FieldType.other].push(index); + } + + hasFieldOfType(type: FieldType): boolean { + return this.fieldIndexByType[type] && this.fieldIndexByType[type].length > 0; + } + + getFields(type?: FieldType): IndexedField[] { + const fields: IndexedField[] = []; + for (let index = 0; index < this.fields.length; index++) { + const field = this.fields[index]; + + if (!type || field.type === type) { + fields.push({ ...field, index }); + } + } + + return fields; + } + + getFieldByIndex(index: number): IndexedField | null { + return this.fields[index] ? { ...this.fields[index], index } : null; + } + + getFirstFieldOfType(type: FieldType): IndexedField | null { + return this.hasFieldOfType(type) + ? { ...this.fields[this.fieldIndexByType[type][0]], index: this.fieldIndexByType[type][0] } + : null; + } + + hasFieldNamed(name: string): boolean { + return this.fieldIndexByName[name] !== undefined; + } + + getFieldByName(name: string): IndexedField | null { + return this.hasFieldNamed(name) + ? { ...this.fields[this.fieldIndexByName[name]], index: this.fieldIndexByName[name] } + : null; + } +} diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 5d5a7b32c60eb..f2af60e353946 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -14,3 +14,4 @@ export { getMappedValue } from './valueMappings'; export * from './validate'; export { getFlotPairs } from './flotPairs'; export * from './object'; +export * from './fieldCache'; diff --git a/packages/grafana-ui/src/utils/processSeriesData.ts b/packages/grafana-ui/src/utils/processSeriesData.ts index 1e78eee3da006..0a8d420fdaa13 100644 --- a/packages/grafana-ui/src/utils/processSeriesData.ts +++ b/packages/grafana-ui/src/utils/processSeriesData.ts @@ -43,16 +43,6 @@ function convertTimeSeriesToSeriesData(timeSeries: TimeSeries): SeriesData { }; } -export const getFirstTimeField = (series: SeriesData): number => { - const { fields } = series; - for (let i = 0; i < fields.length; i++) { - if (fields[i].type === FieldType.time) { - return i; - } - } - return -1; -}; - // PapaParse Dynamic Typing regex: // https://github.com/mholt/PapaParse/blob/master/papaparse.js#L998 const NUMBER = /^\s*-?(\d*\.?\d+|\d+\.?\d*)(e[-+]?\d+)?\s*$/i; diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile index 3980e4bf5d8d5..d72accb0cd924 100644 --- a/packaging/docker/Dockerfile +++ b/packaging/docker/Dockerfile @@ -17,6 +17,7 @@ FROM ${BASE_IMAGE} ARG GF_UID="472" ARG GF_GID="472" +ARG DEBIAN_FRONTEND=noninteractive ENV PATH=/usr/share/grafana/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \ GF_PATHS_CONFIG="/etc/grafana/grafana.ini" \ diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh index a522363089bbb..ceb7c52e2af6e 100755 --- a/packaging/docker/build.sh +++ b/packaging/docker/build.sh @@ -1,4 +1,19 @@ #!/bin/sh +BUILD_FAST=0 + +while [ "$1" != "" ]; do + case "$1" in + "--fast") + BUILD_FAST=1 + echo "Fast build enabled" + shift + ;; + * ) + # unknown param causes args to be passed through to $@ + break + ;; + esac +done _grafana_tag=${1:-} _docker_repo=${2:-grafana/grafana} @@ -27,19 +42,28 @@ docker_build () { --no-cache=true . } +docker_tag_linux_amd64 () { + repo=$1 + tag=$2 + docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}" +} + # Tag docker images of all architectures docker_tag_all () { repo=$1 tag=$2 - docker tag "${_docker_repo}:${_grafana_version}" "${repo}:${tag}" - docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}" - docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}" + docker_tag_linux_amd64 $1 $2 + if [ $BUILD_FAST = "0" ]; then + docker tag "${_docker_repo}-arm32v7-linux:${_grafana_version}" "${repo}-arm32v7-linux:${tag}" + docker tag "${_docker_repo}-arm64v8-linux:${_grafana_version}" "${repo}-arm64v8-linux:${tag}" + fi } docker_build "debian:stretch-slim" "grafana-latest.linux-x64.tar.gz" "${_docker_repo}:${_grafana_version}" -docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}" -docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}" - +if [ $BUILD_FAST = "0" ]; then + docker_build "arm32v7/debian:stretch-slim" "grafana-latest.linux-armv7.tar.gz" "${_docker_repo}-arm32v7-linux:${_grafana_version}" + docker_build "arm64v8/debian:stretch-slim" "grafana-latest.linux-arm64.tar.gz" "${_docker_repo}-arm64v8-linux:${_grafana_version}" +fi # Tag as 'latest' for official release; otherwise tag as grafana/grafana:master if echo "$_grafana_tag" | grep -q "^v"; then docker_tag_all "${_docker_repo}" "latest" diff --git a/pkg/api/admin_users.go b/pkg/api/admin_users.go index 4ad8a2b84ab55..76193771eb91f 100644 --- a/pkg/api/admin_users.go +++ b/pkg/api/admin_users.go @@ -119,7 +119,7 @@ func (server *HTTPServer) AdminLogoutUser(c *m.ReqContext) Response { return Error(400, "You cannot logout yourself", nil) } - return server.logoutUserFromAllDevicesInternal(userID) + return server.logoutUserFromAllDevicesInternal(c.Req.Context(), userID) } // GET /api/admin/users/:id/auth-tokens diff --git a/pkg/api/api.go b/pkg/api/api.go index a727ed15e9610..df532442f8627 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -283,10 +283,10 @@ func (hs *HTTPServer) registerRoutes() { // Dashboard apiRoute.Group("/dashboards", func(dashboardRoute routing.RouteRegister) { - dashboardRoute.Get("/uid/:uid", Wrap(GetDashboard)) + dashboardRoute.Get("/uid/:uid", Wrap(hs.GetDashboard)) dashboardRoute.Delete("/uid/:uid", Wrap(DeleteDashboardByUID)) - dashboardRoute.Get("/db/:slug", Wrap(GetDashboard)) + dashboardRoute.Get("/db/:slug", Wrap(hs.GetDashboard)) dashboardRoute.Delete("/db/:slug", Wrap(DeleteDashboardBySlug)) dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff)) diff --git a/pkg/api/dashboard.go b/pkg/api/dashboard.go index a9d78383045b8..212b568428091 100644 --- a/pkg/api/dashboard.go +++ b/pkg/api/dashboard.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path" + "path/filepath" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" @@ -47,7 +48,7 @@ func dashboardGuardianResponse(err error) Response { return Error(403, "Access denied to this dashboard", nil) } -func GetDashboard(c *m.ReqContext) Response { +func (hs *HTTPServer) GetDashboard(c *m.ReqContext) Response { dash, rsp := getDashboardHelper(c.OrgId, c.Params(":slug"), 0, c.Params(":uid")) if rsp != nil { return rsp @@ -106,14 +107,22 @@ func GetDashboard(c *m.ReqContext) Response { meta.FolderUrl = query.Result.GetUrl() } - isDashboardProvisioned := &m.IsDashboardProvisionedQuery{DashboardId: dash.Id} - err = bus.Dispatch(isDashboardProvisioned) + provisioningData, err := dashboards.NewProvisioningService().GetProvisionedDashboardDataByDashboardId(dash.Id) if err != nil { return Error(500, "Error while checking if dashboard is provisioned", err) } - if isDashboardProvisioned.Result { + if provisioningData != nil { meta.Provisioned = true + meta.ProvisionedExternalId, err = filepath.Rel( + hs.ProvisioningService.GetDashboardProvisionerResolvedPath(provisioningData.Name), + provisioningData.ExternalId, + ) + if err != nil { + // Not sure when this could happen so not sure how to better handle this. Right now ProvisionedExternalId + // is for better UX, showing in Save/Delete dialogs and so it won't break anything if it is empty. + hs.log.Warn("Failed to create ProvisionedExternalId", "err", err) + } } // make sure db version is in sync with json model version diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index ec6c404e6412f..a3ddf76a7b804 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -11,6 +11,7 @@ import ( m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/provisioning" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -43,8 +44,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -198,8 +199,8 @@ func TestDashboardApiEndpoint(t *testing.T) { fakeDash.HasAcl = true setting.ViewersCanEdit = false - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -235,6 +236,10 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + } + // This tests six scenarios: // 1. user is an org viewer AND has no permissions for this dashboard // 2. user is an org editor AND has no permissions for this dashboard @@ -247,7 +252,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_VIEWER loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by slug", func() { @@ -260,7 +265,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by uid", func() { @@ -305,7 +310,7 @@ func TestDashboardApiEndpoint(t *testing.T) { role := m.ROLE_EDITOR loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/db/child-dash", "/api/dashboards/db/:slug", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by slug", func() { @@ -318,7 +323,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/abcdefghi", "/api/dashboards/uid/:uid", role, func(sc *scenarioContext) { - sc.handlerFunc = GetDashboard + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() Convey("Should lookup dashboard by uid", func() { @@ -636,8 +641,8 @@ func TestDashboardApiEndpoint(t *testing.T) { dashTwo.FolderId = 3 dashTwo.HasAcl = false - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -766,8 +771,8 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = false + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = nil return nil }) @@ -905,12 +910,12 @@ func TestDashboardApiEndpoint(t *testing.T) { return nil }) bus.AddHandler("test", func(query *m.GetDashboardQuery) error { - query.Result = &m.Dashboard{Id: 1} + query.Result = &m.Dashboard{Id: 1, Data: &simplejson.Json{}} return nil }) - bus.AddHandler("test", func(query *m.IsDashboardProvisionedQuery) error { - query.Result = true + bus.AddHandler("test", func(query *m.GetProvisionedDashboardDataByIdQuery) error { + query.Result = &m.DashboardProvisioning{ExternalId: "/tmp/grafana/dashboards/test/dashboard1.json"} return nil }) @@ -940,11 +945,32 @@ func TestDashboardApiEndpoint(t *testing.T) { So(result.Get("error").MustString(), ShouldEqual, m.ErrDashboardCannotDeleteProvisionedDashboard.Error()) }) }) + + loggedInUserScenarioWithRole("When calling GET on", "GET", "/api/dashboards/uid/dash", "/api/dashboards/uid/:uid", m.ROLE_EDITOR, func(sc *scenarioContext) { + mock := provisioning.NewProvisioningServiceMock() + mock.GetDashboardProvisionerResolvedPathFunc = func(name string) string { + return "/tmp/grafana/dashboards" + } + + dash := GetDashboardShouldReturn200WithConfig(sc, mock) + + Convey("Should return relative path to provisioning file", func() { + So(dash.Meta.ProvisionedExternalId, ShouldEqual, "test/dashboard1.json") + }) + }) }) } -func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { - CallGetDashboard(sc) +func GetDashboardShouldReturn200WithConfig(sc *scenarioContext, provisioningService ProvisioningService) dtos.DashboardFullWithMeta { + if provisioningService == nil { + provisioningService = provisioning.NewProvisioningServiceMock() + } + + hs := &HTTPServer{ + Cfg: setting.NewCfg(), + ProvisioningService: provisioningService, + } + CallGetDashboard(sc, hs) So(sc.resp.Code, ShouldEqual, 200) @@ -955,8 +981,13 @@ func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta return dash } -func CallGetDashboard(sc *scenarioContext) { - sc.handlerFunc = GetDashboard +func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta { + return GetDashboardShouldReturn200WithConfig(sc, nil) +} + +func CallGetDashboard(sc *scenarioContext, hs *HTTPServer) { + + sc.handlerFunc = hs.GetDashboard sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() } diff --git a/pkg/api/dtos/dashboard.go b/pkg/api/dtos/dashboard.go index 39a6dca580d95..c30e0dbcad249 100644 --- a/pkg/api/dtos/dashboard.go +++ b/pkg/api/dtos/dashboard.go @@ -7,28 +7,29 @@ import ( ) type DashboardMeta struct { - IsStarred bool `json:"isStarred,omitempty"` - IsHome bool `json:"isHome,omitempty"` - IsSnapshot bool `json:"isSnapshot,omitempty"` - Type string `json:"type,omitempty"` - CanSave bool `json:"canSave"` - CanEdit bool `json:"canEdit"` - CanAdmin bool `json:"canAdmin"` - CanStar bool `json:"canStar"` - Slug string `json:"slug"` - Url string `json:"url"` - Expires time.Time `json:"expires"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - UpdatedBy string `json:"updatedBy"` - CreatedBy string `json:"createdBy"` - Version int `json:"version"` - HasAcl bool `json:"hasAcl"` - IsFolder bool `json:"isFolder"` - FolderId int64 `json:"folderId"` - FolderTitle string `json:"folderTitle"` - FolderUrl string `json:"folderUrl"` - Provisioned bool `json:"provisioned"` + IsStarred bool `json:"isStarred,omitempty"` + IsHome bool `json:"isHome,omitempty"` + IsSnapshot bool `json:"isSnapshot,omitempty"` + Type string `json:"type,omitempty"` + CanSave bool `json:"canSave"` + CanEdit bool `json:"canEdit"` + CanAdmin bool `json:"canAdmin"` + CanStar bool `json:"canStar"` + Slug string `json:"slug"` + Url string `json:"url"` + Expires time.Time `json:"expires"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + UpdatedBy string `json:"updatedBy"` + CreatedBy string `json:"createdBy"` + Version int `json:"version"` + HasAcl bool `json:"hasAcl"` + IsFolder bool `json:"isFolder"` + FolderId int64 `json:"folderId"` + FolderTitle string `json:"folderTitle"` + FolderUrl string `json:"folderUrl"` + Provisioned bool `json:"provisioned"` + ProvisionedExternalId string `json:"provisionedExternalId"` } type DashboardFullWithMeta struct { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index a8e92014a8f1e..7c84d579e4a95 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -25,13 +25,12 @@ import ( "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/provisioning" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" - macaron "gopkg.in/macaron.v1" + "gopkg.in/macaron.v1" ) func init() { @@ -42,6 +41,13 @@ func init() { }) } +type ProvisioningService interface { + ProvisionDatasources() error + ProvisionNotifications() error + ProvisionDashboards() error + GetDashboardProvisionerResolvedPath(name string) string +} + type HTTPServer struct { log log.Logger macaron *macaron.Macaron @@ -49,17 +55,17 @@ type HTTPServer struct { streamManager *live.StreamManager httpSrv *http.Server - 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:""` - AuthTokenService models.UserTokenService `inject:""` - QuotaService *quota.QuotaService `inject:""` - RemoteCacheService *remotecache.RemoteCache `inject:""` - ProvisioningService provisioning.ProvisioningService `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:""` + AuthTokenService models.UserTokenService `inject:""` + QuotaService *quota.QuotaService `inject:""` + RemoteCacheService *remotecache.RemoteCache `inject:""` + ProvisioningService ProvisioningService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/api/login.go b/pkg/api/login.go index 65ace1b2b835e..ebf4cc8db07ce 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -131,7 +131,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { hs.log.Error("user login with nil user") } - userToken, err := hs.AuthTokenService.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) + userToken, err := hs.AuthTokenService.CreateToken(c.Req.Context(), user.Id, c.RemoteAddr(), c.Req.UserAgent()) if err != nil { hs.log.Error("failed to create auth token", "error", err) } @@ -140,7 +140,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { } func (hs *HTTPServer) Logout(c *m.ReqContext) { - if err := hs.AuthTokenService.RevokeToken(c.UserToken); err != nil && err != m.ErrUserTokenNotFound { + if err := hs.AuthTokenService.RevokeToken(c.Req.Context(), c.UserToken); err != nil && err != m.ErrUserTokenNotFound { hs.log.Error("failed to revoke auth token", "error", err) } diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b86d1834844f7..fe79fa565950b 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -101,8 +101,14 @@ func (proxy *DataSourceProxy) HandleRequest() { opentracing.HTTPHeaders, opentracing.HTTPHeadersCarrier(proxy.ctx.Req.Request.Header)) + originalSetCookie := proxy.ctx.Resp.Header().Get("Set-Cookie") + reverseProxy.ServeHTTP(proxy.ctx.Resp, proxy.ctx.Req.Request) proxy.ctx.Resp.Header().Del("Set-Cookie") + + if originalSetCookie != "" { + proxy.ctx.Resp.Header().Set("Set-Cookie", originalSetCookie) + } } func (proxy *DataSourceProxy) addTraceFromHeaderValue(span opentracing.Span, headerName string, tagName string) { diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index e9e526fb7c39d..c3eccb4a57c96 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -3,13 +3,15 @@ package pluginproxy import ( "bytes" "fmt" - "github.com/grafana/grafana/pkg/components/securejsondata" "io/ioutil" "net/http" + "net/http/httptest" "net/url" "testing" "time" + "github.com/grafana/grafana/pkg/components/securejsondata" + "golang.org/x/oauth2" macaron "gopkg.in/macaron.v1" @@ -496,9 +498,73 @@ func TestDSRouteRule(t *testing.T) { runDatasourceAuthTest(test) } }) + + Convey("HandleRequest()", func() { + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.SetCookie(w, &http.Cookie{Name: "flavor", Value: "chocolateChip"}) + w.WriteHeader(200) + w.Write([]byte("I am the backend")) + })) + defer backend.Close() + + plugin := &plugins.DataSourcePlugin{} + ds := &m.DataSource{Url: backend.URL, Type: m.DS_GRAPHITE} + + responseRecorder := &CloseNotifierResponseRecorder{ + ResponseRecorder: httptest.NewRecorder(), + } + defer responseRecorder.Close() + + setupCtx := func(fn func(http.ResponseWriter)) *m.ReqContext { + responseWriter := macaron.NewResponseWriter("GET", responseRecorder) + if fn != nil { + fn(responseWriter) + } + + return &m.ReqContext{ + SignedInUser: &m.SignedInUser{}, + Context: &macaron.Context{ + Req: macaron.Request{ + Request: httptest.NewRequest("GET", "/render", nil), + }, + Resp: responseWriter, + }, + } + } + + Convey("When response header Set-Cookie is not set should remove proxied Set-Cookie header", func() { + ctx := setupCtx(nil) + proxy := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) + proxy.HandleRequest() + So(proxy.ctx.Resp.Header().Get("Set-Cookie"), ShouldBeEmpty) + }) + + Convey("When response header Set-Cookie is set should remove proxied Set-Cookie header and restore the original Set-Cookie header", func() { + ctx := setupCtx(func(w http.ResponseWriter) { + w.Header().Set("Set-Cookie", "important_cookie=important_value") + }) + proxy := NewDataSourceProxy(ds, plugin, ctx, "/render", &setting.Cfg{}) + proxy.HandleRequest() + So(proxy.ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "important_cookie=important_value") + }) + }) }) } +type CloseNotifierResponseRecorder struct { + *httptest.ResponseRecorder + closeChan chan bool +} + +func (r *CloseNotifierResponseRecorder) CloseNotify() <-chan bool { + r.closeChan = make(chan bool) + return r.closeChan +} + +func (r *CloseNotifierResponseRecorder) Close() { + close(r.closeChan) +} + // getDatasourceProxiedRequest is a helper for easier setup of tests based on global config and ReqContext. func getDatasourceProxiedRequest(ctx *m.ReqContext, cfg *setting.Cfg) *http.Request { plugin := &plugins.DataSourcePlugin{} diff --git a/pkg/api/user_token.go b/pkg/api/user_token.go index 2f74eedea5dce..3e53a003bd893 100644 --- a/pkg/api/user_token.go +++ b/pkg/api/user_token.go @@ -1,6 +1,7 @@ package api import ( + "context" "time" "github.com/grafana/grafana/pkg/api/dtos" @@ -19,7 +20,7 @@ func (server *HTTPServer) RevokeUserAuthToken(c *models.ReqContext, cmd models.R return server.revokeUserAuthTokenInternal(c, c.UserId, cmd) } -func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Response { +func (server *HTTPServer) logoutUserFromAllDevicesInternal(ctx context.Context, userID int64) Response { userQuery := models.GetUserByIdQuery{Id: userID} if err := bus.Dispatch(&userQuery); err != nil { @@ -29,7 +30,7 @@ func (server *HTTPServer) logoutUserFromAllDevicesInternal(userID int64) Respons return Error(500, "Could not read user from database", err) } - err := server.AuthTokenService.RevokeAllUserTokens(userID) + err := server.AuthTokenService.RevokeAllUserTokens(ctx, userID) if err != nil { return Error(500, "Failed to logout user", err) } @@ -49,7 +50,7 @@ func (server *HTTPServer) getUserAuthTokensInternal(c *models.ReqContext, userID return Error(500, "Failed to get user", err) } - tokens, err := server.AuthTokenService.GetUserTokens(userID) + tokens, err := server.AuthTokenService.GetUserTokens(c.Req.Context(), userID) if err != nil { return Error(500, "Failed to get user auth tokens", err) } @@ -84,7 +85,7 @@ func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, user return Error(500, "Failed to get user", err) } - token, err := server.AuthTokenService.GetUserToken(userID, cmd.AuthTokenId) + token, err := server.AuthTokenService.GetUserToken(c.Req.Context(), userID, cmd.AuthTokenId) if err != nil { if err == models.ErrUserTokenNotFound { return Error(404, "User auth token not found", err) @@ -96,7 +97,7 @@ func (server *HTTPServer) revokeUserAuthTokenInternal(c *models.ReqContext, user return Error(400, "Cannot revoke active user auth token", nil) } - err = server.AuthTokenService.RevokeToken(token) + err = server.AuthTokenService.RevokeToken(c.Req.Context(), token) if err != nil { if err == models.ErrUserTokenNotFound { return Error(404, "User auth token not found", err) diff --git a/pkg/api/user_token_test.go b/pkg/api/user_token_test.go index 111070dca92c5..aa5bc47f93e96 100644 --- a/pkg/api/user_token_test.go +++ b/pkg/api/user_token_test.go @@ -1,6 +1,7 @@ package api import ( + "context" "testing" "time" @@ -75,7 +76,7 @@ func TestUserTokenApiEndpoint(t *testing.T) { token := &m.UserToken{Id: 1} revokeUserAuthTokenInternalScenario("Should be successful", cmd, 200, token, func(sc *scenarioContext) { - sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) { + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*m.UserToken, error) { return &m.UserToken{Id: 2}, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -93,7 +94,7 @@ func TestUserTokenApiEndpoint(t *testing.T) { token := &m.UserToken{Id: 2} revokeUserAuthTokenInternalScenario("Should not be successful", cmd, TestUserID, token, func(sc *scenarioContext) { - sc.userAuthTokenService.GetUserTokenProvider = func(userId, userTokenId int64) (*m.UserToken, error) { + sc.userAuthTokenService.GetUserTokenProvider = func(ctx context.Context, userId, userTokenId int64) (*m.UserToken, error) { return token, nil } sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() @@ -126,7 +127,7 @@ func TestUserTokenApiEndpoint(t *testing.T) { SeenAt: time.Now().Unix(), }, } - sc.userAuthTokenService.GetUserTokensProvider = func(userId int64) ([]*m.UserToken, error) { + sc.userAuthTokenService.GetUserTokensProvider = func(ctx context.Context, userId int64) ([]*m.UserToken, error) { return tokens, nil } sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() @@ -226,7 +227,7 @@ func logoutUserFromAllDevicesInternalScenario(desc string, userId int64, fn scen sc.context.OrgId = TestOrgID sc.context.OrgRole = m.ROLE_ADMIN - return hs.logoutUserFromAllDevicesInternal(userId) + return hs.logoutUserFromAllDevicesInternal(context.Background(), userId) }) sc.m.Post("/", sc.defaultHandler) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index bec1d51bff468..99cef15e50e3f 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -144,7 +144,7 @@ func downloadFile(pluginName, filePath, url string) (err error) { } }() - resp, err := http.Get(url) + resp, err := http.Get(url) // #nosec if err != nil { return err } @@ -167,7 +167,7 @@ func extractFiles(body []byte, pluginName string, filePath string) error { newFile := path.Join(filePath, RemoveGitBuildFromName(pluginName, zf.Name)) if zf.FileInfo().IsDir() { - err := os.Mkdir(newFile, 0777) + err := os.Mkdir(newFile, 0755) if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index c825ea0d8ba09..f988b0b576337 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -174,7 +174,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext return false } - token, err := authTokenService.LookupToken(rawToken) + token, err := authTokenService.LookupToken(ctx.Req.Context(), rawToken) if err != nil { ctx.Logger.Error("failed to look up user based on cookie", "error", err) WriteSessionCookie(ctx, "", -1) @@ -191,7 +191,7 @@ func initContextWithToken(authTokenService m.UserTokenService, ctx *m.ReqContext ctx.IsSignedIn = true ctx.UserToken = token - rotated, err := authTokenService.TryRotateToken(token, ctx.RemoteAddr(), ctx.Req.UserAgent()) + rotated, err := authTokenService.TryRotateToken(ctx.Req.Context(), token, ctx.RemoteAddr(), ctx.Req.UserAgent()) if err != nil { ctx.Logger.Error("failed to rotate token", "error", err) return true diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 92d3da0896f2c..e59e017398a78 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "encoding/json" "fmt" "net/http" @@ -156,7 +157,7 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: unhashedToken, @@ -185,14 +186,14 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", }, nil } - sc.userAuthTokenService.TryRotateTokenProvider = func(userToken *m.UserToken, clientIP, userAgent string) (bool, error) { + sc.userAuthTokenService.TryRotateTokenProvider = func(ctx context.Context, userToken *m.UserToken, clientIP, userAgent string) (bool, error) { userToken.UnhashedToken = "rotated" return true, nil } @@ -227,7 +228,7 @@ func TestMiddlewareContext(t *testing.T) { middlewareScenario(t, "Invalid/expired auth token in cookie", func(sc *scenarioContext) { sc.withTokenSessionCookie("token") - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return nil, m.ErrUserTokenNotFound } diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index f307376331ece..e74b6e8451c90 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "fmt" "testing" @@ -23,7 +24,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { return nil }) - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 0, UnhashedToken: "", @@ -49,7 +50,7 @@ func TestOrgRedirectMiddleware(t *testing.T) { return nil }) - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index c3b448c9f6a07..c6c8a1fd4d336 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -1,6 +1,7 @@ package middleware import ( + "context" "testing" "github.com/grafana/grafana/pkg/bus" @@ -87,7 +88,7 @@ func TestMiddlewareQuota(t *testing.T) { return nil }) - sc.userAuthTokenService.LookupTokenProvider = func(unhashedToken string) (*m.UserToken, error) { + sc.userAuthTokenService.LookupTokenProvider = func(ctx context.Context, unhashedToken string) (*m.UserToken, error) { return &m.UserToken{ UserId: 12, UnhashedToken: "", diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index e54d0c1145349..60677c9b6f6ea 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -323,15 +323,13 @@ type GetDashboardSlugByIdQuery struct { Result string } -type IsDashboardProvisionedQuery struct { +type GetProvisionedDashboardDataByIdQuery struct { DashboardId int64 - - Result bool + Result *DashboardProvisioning } type GetProvisionedDashboardDataQuery struct { - Name string - + Name string Result []*DashboardProvisioning } diff --git a/pkg/models/user_token.go b/pkg/models/user_token.go index 8c3e7985995e7..b07bd50811487 100644 --- a/pkg/models/user_token.go +++ b/pkg/models/user_token.go @@ -1,6 +1,7 @@ package models import ( + "context" "errors" ) @@ -31,12 +32,12 @@ type RevokeAuthTokenCmd struct { // UserTokenService are used for generating and validating user tokens type UserTokenService interface { - CreateToken(userId int64, clientIP, userAgent string) (*UserToken, error) - LookupToken(unhashedToken string) (*UserToken, error) - TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error) - RevokeToken(token *UserToken) error - RevokeAllUserTokens(userId int64) error - ActiveTokenCount() (int64, error) - GetUserToken(userId, userTokenId int64) (*UserToken, error) - GetUserTokens(userId int64) ([]*UserToken, error) + CreateToken(ctx context.Context, userId int64, clientIP, userAgent string) (*UserToken, error) + LookupToken(ctx context.Context, unhashedToken string) (*UserToken, error) + TryRotateToken(ctx context.Context, token *UserToken, clientIP, userAgent string) (bool, error) + RevokeToken(ctx context.Context, token *UserToken) error + RevokeAllUserTokens(ctx context.Context, userId int64) error + ActiveTokenCount(ctx context.Context) (int64, error) + GetUserToken(ctx context.Context, userId, userTokenId int64) (*UserToken, error) + GetUserTokens(ctx context.Context, userId int64) ([]*UserToken, error) } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 1a0a910a5d329..c28ac49b8945d 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,7 +3,6 @@ package alerting import ( "errors" "fmt" - "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -127,7 +126,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { renderOpts := rendering.Opts{ Width: 1000, Height: 500, - Timeout: time.Duration(setting.AlertingEvaluationTimeout.Seconds() * 0.9), + Timeout: setting.AlertingEvaluationTimeout, OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, ConcurrentLimit: setting.AlertingRenderLimit, diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 740e5081668d0..dc9936f2f3ffd 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -1,6 +1,7 @@ package auth import ( + "context" "crypto/sha256" "encoding/hex" "time" @@ -35,14 +36,24 @@ func (s *UserAuthTokenService) Init() error { return nil } -func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) { - var model userAuthToken - count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model) +func (s *UserAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, error) { + + var count int64 + var err error + err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + var model userAuthToken + count, err = dbSession.Where(`created_at > ? AND rotated_at > ?`, + s.createdAfterParam(), + s.rotatedAfterParam()). + Count(&model) + + return err + }) return count, err } -func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) { +func (s *UserAuthTokenService) CreateToken(ctx context.Context, userId int64, clientIP, userAgent string) (*models.UserToken, error) { clientIP = util.ParseIPAddress(clientIP) token, err := util.RandomHex(16) if err != nil { @@ -65,7 +76,12 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str SeenAt: 0, AuthTokenSeen: false, } - _, err = s.SQLStore.NewSession().Insert(&userAuthToken) + + err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + _, err = dbSession.Insert(&userAuthToken) + return err + }) + if err != nil { return nil, err } @@ -80,14 +96,27 @@ func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent str return &userToken, err } -func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) { +func (s *UserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*models.UserToken, error) { hashedToken := hashToken(unhashedToken) if setting.Env == setting.DEV { s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) } var model userAuthToken - exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model) + var exists bool + var err error + err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + exists, err = dbSession.Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", + hashedToken, + hashedToken, + s.createdAfterParam(), + s.rotatedAfterParam()). + Get(&model) + + return err + + }) + if err != nil { return nil, err } @@ -100,7 +129,18 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo modelCopy := model modelCopy.AuthTokenSeen = false expireBefore := getTime().Add(-urgentRotateTime).Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", modelCopy.Id, modelCopy.PrevAuthToken, expireBefore).AllCols().Update(&modelCopy) + + var affectedRows int64 + err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + affectedRows, err = dbSession.Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", + modelCopy.Id, + modelCopy.PrevAuthToken, + expireBefore). + AllCols().Update(&modelCopy) + + return err + }) + if err != nil { return nil, err } @@ -116,7 +156,17 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo modelCopy := model modelCopy.AuthTokenSeen = true modelCopy.SeenAt = getTime().Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy) + + var affectedRows int64 + err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + affectedRows, err = dbSession.Where("id = ? AND auth_token = ?", + modelCopy.Id, + modelCopy.AuthToken). + AllCols().Update(&modelCopy) + + return err + }) + if err != nil { return nil, err } @@ -140,7 +190,7 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo return &userToken, err } -func (s *UserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) { +func (s *UserAuthTokenService) TryRotateToken(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) { if token == nil { return false, nil } @@ -183,12 +233,21 @@ func (s *UserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, rotated_at = ? WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) + var affected int64 + err = s.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + res, err := dbSession.Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) + if err != nil { + return err + } + + affected, err = res.RowsAffected() + return err + }) + if err != nil { return false, err } - affected, _ := res.RowsAffected() s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId) if affected > 0 { model.UnhashedToken = newToken @@ -199,14 +258,20 @@ func (s *UserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, return false, nil } -func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error { +func (s *UserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken) error { if token == nil { return models.ErrUserTokenNotFound } model := userAuthTokenFromUserToken(token) - rowsAffected, err := s.SQLStore.NewSession().Delete(model) + var rowsAffected int64 + var err error + err = s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + rowsAffected, err = dbSession.Delete(model) + return err + }) + if err != nil { return err } @@ -221,55 +286,71 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error { return nil } -func (s *UserAuthTokenService) RevokeAllUserTokens(userId int64) error { - sql := `DELETE from user_auth_token WHERE user_id = ?` - res, err := s.SQLStore.NewSession().Exec(sql, userId) - if err != nil { - return err - } +func (s *UserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error { + return s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + sql := `DELETE from user_auth_token WHERE user_id = ?` + res, err := dbSession.Exec(sql, userId) + if err != nil { + return err + } - affected, err := res.RowsAffected() - if err != nil { - return err - } + affected, err := res.RowsAffected() + if err != nil { + return err + } - s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected) + s.log.Debug("all user tokens for user revoked", "userId", userId, "count", affected) - return nil + return err + }) } -func (s *UserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) { - var token userAuthToken - exists, err := s.SQLStore.NewSession().Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token) - if err != nil { - return nil, err - } - - if !exists { - return nil, models.ErrUserTokenNotFound - } +func (s *UserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { var result models.UserToken - token.toUserToken(&result) + err := s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + var token userAuthToken + exists, err := dbSession.Where("id = ? AND user_id = ?", userTokenId, userId).Get(&token) + if err != nil { + return err + } + + if !exists { + return models.ErrUserTokenNotFound + } + + token.toUserToken(&result) + return nil + }) - return &result, nil + return &result, err } -func (s *UserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) { - var tokens []*userAuthToken - err := s.SQLStore.NewSession().Where("user_id = ? AND created_at > ? AND rotated_at > ?", userId, s.createdAfterParam(), s.rotatedAfterParam()).Find(&tokens) - if err != nil { - return nil, err - } +func (s *UserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { result := []*models.UserToken{} - for _, token := range tokens { - var userToken models.UserToken - token.toUserToken(&userToken) - result = append(result, &userToken) - } + err := s.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + var tokens []*userAuthToken + err := dbSession.Where("user_id = ? AND created_at > ? AND rotated_at > ?", + userId, + s.createdAfterParam(), + s.rotatedAfterParam()). + Find(&tokens) + + if err != nil { + return err + } + + for _, token := range tokens { + var userToken models.UserToken + token.toUserToken(&userToken) + result = append(result, &userToken) + } + + return nil + }) - return result, nil + return result, err } func (s *UserAuthTokenService) createdAfterParam() int64 { diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 33eb309ad18dd..b1398834bdc94 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -1,6 +1,7 @@ package auth import ( + "context" "encoding/json" "testing" "time" @@ -26,19 +27,19 @@ func TestUserAuthToken(t *testing.T) { } Convey("When creating token", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(userToken, ShouldNotBeNil) So(userToken.AuthTokenSeen, ShouldBeFalse) Convey("Can count active tokens", func() { - count, err := userAuthTokenService.ActiveTokenCount() + count, err := userAuthTokenService.ActiveTokenCount(context.Background()) So(err, ShouldBeNil) So(count, ShouldEqual, 1) }) Convey("When lookup unhashed token should return user auth token", func() { - userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + userToken, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(userToken, ShouldNotBeNil) So(userToken.UserId, ShouldEqual, userID) @@ -51,13 +52,13 @@ func TestUserAuthToken(t *testing.T) { }) Convey("When lookup hashed token should return user auth token not found error", func() { - userToken, err := userAuthTokenService.LookupToken(userToken.AuthToken) + userToken, err := userAuthTokenService.LookupToken(context.Background(), userToken.AuthToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) So(userToken, ShouldBeNil) }) Convey("revoking existing token should delete token", func() { - err = userAuthTokenService.RevokeToken(userToken) + err = userAuthTokenService.RevokeToken(context.Background(), userToken) So(err, ShouldBeNil) model, err := ctx.getAuthTokenByID(userToken.Id) @@ -66,37 +67,37 @@ func TestUserAuthToken(t *testing.T) { }) Convey("revoking nil token should return error", func() { - err = userAuthTokenService.RevokeToken(nil) + err = userAuthTokenService.RevokeToken(context.Background(), nil) So(err, ShouldEqual, models.ErrUserTokenNotFound) }) Convey("revoking non-existing token should return error", func() { userToken.Id = 1000 - err = userAuthTokenService.RevokeToken(userToken) + err = userAuthTokenService.RevokeToken(context.Background(), userToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) }) Convey("When creating an additional token", func() { - userToken2, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken2, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(userToken2, ShouldNotBeNil) Convey("Can get first user token", func() { - token, err := userAuthTokenService.GetUserToken(userID, userToken.Id) + token, err := userAuthTokenService.GetUserToken(context.Background(), userID, userToken.Id) So(err, ShouldBeNil) So(token, ShouldNotBeNil) So(token.Id, ShouldEqual, userToken.Id) }) Convey("Can get second user token", func() { - token, err := userAuthTokenService.GetUserToken(userID, userToken2.Id) + token, err := userAuthTokenService.GetUserToken(context.Background(), userID, userToken2.Id) So(err, ShouldBeNil) So(token, ShouldNotBeNil) So(token.Id, ShouldEqual, userToken2.Id) }) Convey("Can get user tokens", func() { - tokens, err := userAuthTokenService.GetUserTokens(userID) + tokens, err := userAuthTokenService.GetUserTokens(context.Background(), userID) So(err, ShouldBeNil) So(tokens, ShouldHaveLength, 2) So(tokens[0].Id, ShouldEqual, userToken.Id) @@ -104,7 +105,7 @@ func TestUserAuthToken(t *testing.T) { }) Convey("Can revoke all user tokens", func() { - err := userAuthTokenService.RevokeAllUserTokens(userID) + err := userAuthTokenService.RevokeAllUserTokens(context.Background(), userID) So(err, ShouldBeNil) model, err := ctx.getAuthTokenByID(userToken.Id) @@ -119,24 +120,24 @@ func TestUserAuthToken(t *testing.T) { }) Convey("expires correctly", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) - userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken) + userToken, err = userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) getTime = func() time.Time { return t.Add(time.Hour) } - rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent") + rotated, err := userAuthTokenService.TryRotateToken(context.Background(), userToken, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) - userToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken) + userToken, err = userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) - stillGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + stillGood, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) @@ -148,7 +149,7 @@ func TestUserAuthToken(t *testing.T) { return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour).Add(-time.Second) } - stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken) + stillGood, err = userAuthTokenService.LookupToken(context.Background(), stillGood.UnhashedToken) So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) }) @@ -158,12 +159,12 @@ func TestUserAuthToken(t *testing.T) { return time.Unix(model.RotatedAt, 0).Add(24 * 7 * time.Hour) } - notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + notGood, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) So(notGood, ShouldBeNil) Convey("should not find active token when expired", func() { - count, err := userAuthTokenService.ActiveTokenCount() + count, err := userAuthTokenService.ActiveTokenCount(context.Background()) So(err, ShouldBeNil) So(count, ShouldEqual, 0) }) @@ -178,7 +179,7 @@ func TestUserAuthToken(t *testing.T) { return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour).Add(-time.Second) } - stillGood, err = userAuthTokenService.LookupToken(stillGood.UnhashedToken) + stillGood, err = userAuthTokenService.LookupToken(context.Background(), stillGood.UnhashedToken) So(err, ShouldBeNil) So(stillGood, ShouldNotBeNil) }) @@ -192,20 +193,20 @@ func TestUserAuthToken(t *testing.T) { return time.Unix(model.CreatedAt, 0).Add(24 * 30 * time.Hour) } - notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + notGood, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldEqual, models.ErrUserTokenNotFound) So(notGood, ShouldBeNil) }) }) Convey("can properly rotate tokens", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) prevToken := userToken.AuthToken unhashedPrev := userToken.UnhashedToken - rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + rotated, err := userAuthTokenService.TryRotateToken(context.Background(), userToken, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(rotated, ShouldBeFalse) @@ -224,7 +225,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(time.Hour) } - rotated, err = userAuthTokenService.TryRotateToken(&tok, "192.168.10.12:1234", "a new user agent") + rotated, err = userAuthTokenService.TryRotateToken(context.Background(), &tok, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) @@ -243,13 +244,13 @@ func TestUserAuthToken(t *testing.T) { // ability to auth using an old token - lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + lookedUpUserToken, err := userAuthTokenService.LookupToken(context.Background(), model.UnhashedToken) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue) So(lookedUpUserToken.SeenAt, ShouldEqual, getTime().Unix()) - lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + lookedUpUserToken, err = userAuthTokenService.LookupToken(context.Background(), unhashedPrev) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) So(lookedUpUserToken.Id, ShouldEqual, model.Id) @@ -259,7 +260,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(time.Hour + (2 * time.Minute)) } - lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + lookedUpUserToken, err = userAuthTokenService.LookupToken(context.Background(), unhashedPrev) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) So(lookedUpUserToken.AuthTokenSeen, ShouldBeTrue) @@ -269,7 +270,7 @@ func TestUserAuthToken(t *testing.T) { So(lookedUpModel, ShouldNotBeNil) So(lookedUpModel.AuthTokenSeen, ShouldBeFalse) - rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + rotated, err = userAuthTokenService.TryRotateToken(context.Background(), userToken, "192.168.10.12:1234", "a new user agent") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) @@ -280,11 +281,11 @@ func TestUserAuthToken(t *testing.T) { }) Convey("keeps prev token valid for 1 minute after it is confirmed", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(userToken, ShouldNotBeNil) - lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + lookedUpUserToken, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) @@ -293,7 +294,7 @@ func TestUserAuthToken(t *testing.T) { } prevToken := userToken.UnhashedToken - rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + rotated, err := userAuthTokenService.TryRotateToken(context.Background(), userToken, "1.1.1.1", "firefox") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) @@ -301,25 +302,25 @@ func TestUserAuthToken(t *testing.T) { return t.Add(20 * time.Minute) } - currentUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + currentUserToken, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(currentUserToken, ShouldNotBeNil) - prevUserToken, err := userAuthTokenService.LookupToken(prevToken) + prevUserToken, err := userAuthTokenService.LookupToken(context.Background(), prevToken) So(err, ShouldBeNil) So(prevUserToken, ShouldNotBeNil) }) Convey("will not mark token unseen when prev and current are the same", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(userToken, ShouldNotBeNil) - lookedUpUserToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken) + lookedUpUserToken, err := userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) - lookedUpUserToken, err = userAuthTokenService.LookupToken(userToken.UnhashedToken) + lookedUpUserToken, err = userAuthTokenService.LookupToken(context.Background(), userToken.UnhashedToken) So(err, ShouldBeNil) So(lookedUpUserToken, ShouldNotBeNil) @@ -330,7 +331,7 @@ func TestUserAuthToken(t *testing.T) { }) Convey("Rotate token", func() { - userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + userToken, err := userAuthTokenService.CreateToken(context.Background(), userID, "192.168.10.11:1234", "some user agent") So(err, ShouldBeNil) So(userToken, ShouldNotBeNil) @@ -345,7 +346,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(10 * time.Minute) } - rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + rotated, err := userAuthTokenService.TryRotateToken(context.Background(), userToken, "1.1.1.1", "firefox") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) @@ -366,7 +367,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(20 * time.Minute) } - rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + rotated, err = userAuthTokenService.TryRotateToken(context.Background(), userToken, "1.1.1.1", "firefox") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) @@ -385,7 +386,7 @@ func TestUserAuthToken(t *testing.T) { return t.Add(2 * time.Minute) } - rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + rotated, err := userAuthTokenService.TryRotateToken(context.Background(), userToken, "1.1.1.1", "firefox") So(err, ShouldBeNil) So(rotated, ShouldBeTrue) diff --git a/pkg/services/auth/testing.go b/pkg/services/auth/testing.go index 68e65466c3d6f..378a68b053c10 100644 --- a/pkg/services/auth/testing.go +++ b/pkg/services/auth/testing.go @@ -1,81 +1,85 @@ package auth -import "github.com/grafana/grafana/pkg/models" +import ( + "context" + + "github.com/grafana/grafana/pkg/models" +) type FakeUserAuthTokenService struct { - CreateTokenProvider func(userId int64, clientIP, userAgent string) (*models.UserToken, error) - TryRotateTokenProvider func(token *models.UserToken, clientIP, userAgent string) (bool, error) - LookupTokenProvider func(unhashedToken string) (*models.UserToken, error) - RevokeTokenProvider func(token *models.UserToken) error - RevokeAllUserTokensProvider func(userId int64) error - ActiveAuthTokenCount func() (int64, error) - GetUserTokenProvider func(userId, userTokenId int64) (*models.UserToken, error) - GetUserTokensProvider func(userId int64) ([]*models.UserToken, error) + CreateTokenProvider func(ctx context.Context, userId int64, clientIP, userAgent string) (*models.UserToken, error) + TryRotateTokenProvider func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) + LookupTokenProvider func(ctx context.Context, unhashedToken string) (*models.UserToken, error) + RevokeTokenProvider func(ctx context.Context, token *models.UserToken) error + RevokeAllUserTokensProvider func(ctx context.Context, userId int64) error + ActiveAuthTokenCount func(ctx context.Context) (int64, error) + GetUserTokenProvider func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) + GetUserTokensProvider func(ctx context.Context, userId int64) ([]*models.UserToken, error) } func NewFakeUserAuthTokenService() *FakeUserAuthTokenService { return &FakeUserAuthTokenService{ - CreateTokenProvider: func(userId int64, clientIP, userAgent string) (*models.UserToken, error) { + CreateTokenProvider: func(ctx context.Context, userId int64, clientIP, userAgent string) (*models.UserToken, error) { return &models.UserToken{ UserId: 0, UnhashedToken: "", }, nil }, - TryRotateTokenProvider: func(token *models.UserToken, clientIP, userAgent string) (bool, error) { + TryRotateTokenProvider: func(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) { return false, nil }, - LookupTokenProvider: func(unhashedToken string) (*models.UserToken, error) { + LookupTokenProvider: func(ctx context.Context, unhashedToken string) (*models.UserToken, error) { return &models.UserToken{ UserId: 0, UnhashedToken: "", }, nil }, - RevokeTokenProvider: func(token *models.UserToken) error { + RevokeTokenProvider: func(ctx context.Context, token *models.UserToken) error { return nil }, - RevokeAllUserTokensProvider: func(userId int64) error { + RevokeAllUserTokensProvider: func(ctx context.Context, userId int64) error { return nil }, - ActiveAuthTokenCount: func() (int64, error) { + ActiveAuthTokenCount: func(ctx context.Context) (int64, error) { return 10, nil }, - GetUserTokenProvider: func(userId, userTokenId int64) (*models.UserToken, error) { + GetUserTokenProvider: func(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { return nil, nil }, - GetUserTokensProvider: func(userId int64) ([]*models.UserToken, error) { + GetUserTokensProvider: func(ctx context.Context, userId int64) ([]*models.UserToken, error) { return nil, nil }, } } -func (s *FakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) { - return s.CreateTokenProvider(userId, clientIP, userAgent) +func (s *FakeUserAuthTokenService) CreateToken(ctx context.Context, userId int64, clientIP, userAgent string) (*models.UserToken, error) { + return s.CreateTokenProvider(context.Background(), userId, clientIP, userAgent) } -func (s *FakeUserAuthTokenService) LookupToken(unhashedToken string) (*models.UserToken, error) { - return s.LookupTokenProvider(unhashedToken) +func (s *FakeUserAuthTokenService) LookupToken(ctx context.Context, unhashedToken string) (*models.UserToken, error) { + return s.LookupTokenProvider(context.Background(), unhashedToken) } -func (s *FakeUserAuthTokenService) TryRotateToken(token *models.UserToken, clientIP, userAgent string) (bool, error) { - return s.TryRotateTokenProvider(token, clientIP, userAgent) +func (s *FakeUserAuthTokenService) TryRotateToken(ctx context.Context, token *models.UserToken, clientIP, userAgent string) (bool, error) { + return s.TryRotateTokenProvider(context.Background(), token, clientIP, userAgent) } -func (s *FakeUserAuthTokenService) RevokeToken(token *models.UserToken) error { - return s.RevokeTokenProvider(token) +func (s *FakeUserAuthTokenService) RevokeToken(ctx context.Context, token *models.UserToken) error { + return s.RevokeTokenProvider(context.Background(), token) } -func (s *FakeUserAuthTokenService) RevokeAllUserTokens(userId int64) error { - return s.RevokeAllUserTokensProvider(userId) +func (s *FakeUserAuthTokenService) RevokeAllUserTokens(ctx context.Context, userId int64) error { + return s.RevokeAllUserTokensProvider(context.Background(), userId) } -func (s *FakeUserAuthTokenService) ActiveTokenCount() (int64, error) { - return s.ActiveAuthTokenCount() +func (s *FakeUserAuthTokenService) ActiveTokenCount(ctx context.Context) (int64, error) { + return s.ActiveAuthTokenCount(context.Background()) } -func (s *FakeUserAuthTokenService) GetUserToken(userId, userTokenId int64) (*models.UserToken, error) { - return s.GetUserTokenProvider(userId, userTokenId) +func (s *FakeUserAuthTokenService) GetUserToken(ctx context.Context, userId, userTokenId int64) (*models.UserToken, error) { + return s.GetUserTokenProvider(context.Background(), userId, userTokenId) } -func (s *FakeUserAuthTokenService) GetUserTokens(userId int64) ([]*models.UserToken, error) { - return s.GetUserTokensProvider(userId) +func (s *FakeUserAuthTokenService) GetUserTokens(ctx context.Context, userId int64) ([]*models.UserToken, error) { + return s.GetUserTokensProvider(context.Background(), userId) } diff --git a/pkg/services/auth/token_cleanup.go b/pkg/services/auth/token_cleanup.go index 1fe0996aa4cb7..671d3d7f5b75d 100644 --- a/pkg/services/auth/token_cleanup.go +++ b/pkg/services/auth/token_cleanup.go @@ -3,6 +3,8 @@ package auth import ( "context" "time" + + "github.com/grafana/grafana/pkg/services/sqlstore" ) func (srv *UserAuthTokenService) Run(ctx context.Context) error { @@ -11,21 +13,22 @@ func (srv *UserAuthTokenService) Run(ctx context.Context) error { maxLifetime := time.Duration(srv.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() { - srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime) + srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime) }) + if err != nil { - srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err) + srv.log.Error("failed to lock and execute cleanup of expired auth token", "error", err) } for { select { case <-ticker.C: err := srv.ServerLockService.LockAndExecute(ctx, "cleanup expired auth tokens", time.Hour*12, func() { - srv.deleteExpiredTokens(maxInactiveLifetime, maxLifetime) + srv.deleteExpiredTokens(ctx, maxInactiveLifetime, maxLifetime) }) if err != nil { - srv.log.Error("failed to lock and execite cleanup of expired auth token", "erro", err) + srv.log.Error("failed to lock and execute cleanup of expired auth token", "error", err) } case <-ctx.Done(): @@ -34,24 +37,30 @@ func (srv *UserAuthTokenService) Run(ctx context.Context) error { } } -func (srv *UserAuthTokenService) deleteExpiredTokens(maxInactiveLifetime, maxLifetime time.Duration) (int64, error) { +func (srv *UserAuthTokenService) deleteExpiredTokens(ctx context.Context, maxInactiveLifetime, maxLifetime time.Duration) (int64, error) { createdBefore := getTime().Add(-maxLifetime) rotatedBefore := getTime().Add(-maxInactiveLifetime) srv.log.Debug("starting cleanup of expired auth tokens", "createdBefore", createdBefore, "rotatedBefore", rotatedBefore) - sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?` - res, err := srv.SQLStore.NewSession().Exec(sql, createdBefore.Unix(), rotatedBefore.Unix()) - if err != nil { - return 0, err - } + var affected int64 + err := srv.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + sql := `DELETE from user_auth_token WHERE created_at <= ? OR rotated_at <= ?` + res, err := dbSession.Exec(sql, createdBefore.Unix(), rotatedBefore.Unix()) + if err != nil { + return err + } - affected, err := res.RowsAffected() - if err != nil { - srv.log.Error("failed to cleanup expired auth tokens", "error", err) - return 0, nil - } + affected, err = res.RowsAffected() + if err != nil { + srv.log.Error("failed to cleanup expired auth tokens", "error", err) + return nil + } + + srv.log.Debug("cleanup of expired auth tokens done", "count", affected) + + return nil + }) - srv.log.Debug("cleanup of expired auth tokens done", "count", affected) return affected, err } diff --git a/pkg/services/auth/token_cleanup_test.go b/pkg/services/auth/token_cleanup_test.go index 410764d3f8dba..2df42eb724ca3 100644 --- a/pkg/services/auth/token_cleanup_test.go +++ b/pkg/services/auth/token_cleanup_test.go @@ -1,6 +1,7 @@ package auth import ( + "context" "fmt" "testing" "time" @@ -40,7 +41,7 @@ func TestUserAuthTokenCleanup(t *testing.T) { insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), from.Unix()) } - affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour) + affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 7*24*time.Hour, 30*24*time.Hour) So(err, ShouldBeNil) So(affected, ShouldEqual, 3) }) @@ -60,7 +61,7 @@ func TestUserAuthTokenCleanup(t *testing.T) { insertToken(fmt.Sprintf("newA%d", i), fmt.Sprintf("newB%d", i), from.Unix(), fromRotate.Unix()) } - affected, err := ctx.tokenService.deleteExpiredTokens(7*24*time.Hour, 30*24*time.Hour) + affected, err := ctx.tokenService.deleteExpiredTokens(context.Background(), 7*24*time.Hour, 30*24*time.Hour) So(err, ShouldBeNil) So(affected, ShouldEqual, 3) }) diff --git a/pkg/services/dashboards/dashboard_service.go b/pkg/services/dashboards/dashboard_service.go index cdf3d93a80022..884d993f43c6d 100644 --- a/pkg/services/dashboards/dashboard_service.go +++ b/pkg/services/dashboards/dashboard_service.go @@ -24,6 +24,7 @@ type DashboardProvisioningService interface { SaveProvisionedDashboard(dto *SaveDashboardDTO, provisioning *models.DashboardProvisioning) (*models.Dashboard, error) SaveFolderForProvisionedDashboards(*SaveDashboardDTO) (*models.Dashboard, error) GetProvisionedDashboardData(name string) ([]*models.DashboardProvisioning, error) + GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) UnprovisionDashboard(dashboardId int64) error DeleteProvisionedDashboard(dashboardId int64, orgId int64) error } @@ -37,7 +38,9 @@ var NewService = func() DashboardService { // NewProvisioningService factory for creating a new dashboard provisioning service var NewProvisioningService = func() DashboardProvisioningService { - return &dashboardServiceImpl{} + return &dashboardServiceImpl{ + log: log.New("dashboard-provisioning-service"), + } } type SaveDashboardDTO struct { @@ -65,6 +68,16 @@ func (dr *dashboardServiceImpl) GetProvisionedDashboardData(name string) ([]*mod return cmd.Result, nil } +func (dr *dashboardServiceImpl) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) { + cmd := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashboardId} + err := bus.Dispatch(cmd) + if err != nil { + return nil, err + } + + return cmd.Result, nil +} + func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, validateAlerts bool, validateProvisionedDashboard bool) (*models.SaveDashboardCommand, error) { dash := dto.Dashboard @@ -123,14 +136,12 @@ func (dr *dashboardServiceImpl) buildSaveDashboardCommand(dto *SaveDashboardDTO, } if validateProvisionedDashboard { - isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dash.Id} - err := bus.Dispatch(isDashboardProvisioned) - + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dash.Id) if err != nil { return nil, err } - if isDashboardProvisioned.Result { + if provisionedData != nil { return nil, models.ErrDashboardCannotSaveProvisionedDashboard } } @@ -258,13 +269,12 @@ func (dr *dashboardServiceImpl) DeleteProvisionedDashboard(dashboardId int64, or func (dr *dashboardServiceImpl) deleteDashboard(dashboardId int64, orgId int64, validateProvisionedDashboard bool) error { if validateProvisionedDashboard { - isDashboardProvisioned := &models.IsDashboardProvisionedQuery{DashboardId: dashboardId} - err := bus.Dispatch(isDashboardProvisioned) + provisionedData, err := dr.GetProvisionedDashboardDataByDashboardId(dashboardId) if err != nil { return errutil.Wrap("failed to check if dashboard is provisioned", err) } - if isDashboardProvisioned.Result { + if provisionedData != nil { return models.ErrDashboardCannotDeleteProvisionedDashboard } } diff --git a/pkg/services/dashboards/dashboard_service_test.go b/pkg/services/dashboards/dashboard_service_test.go index 968c3f9a3b49d..db3ef997d65ef 100644 --- a/pkg/services/dashboards/dashboard_service_test.go +++ b/pkg/services/dashboards/dashboard_service_test.go @@ -55,8 +55,8 @@ func TestDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) @@ -85,9 +85,9 @@ func TestDashboardService(t *testing.T) { Convey("Should return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -109,8 +109,8 @@ func TestDashboardService(t *testing.T) { }) Convey("Should return validation error if alert data is invalid", func() { - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) @@ -129,9 +129,9 @@ func TestDashboardService(t *testing.T) { Convey("Should not return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -166,9 +166,9 @@ func TestDashboardService(t *testing.T) { Convey("Should return validation error if dashboard is provisioned", func() { provisioningValidated := false - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true - cmd.Result = true + cmd.Result = &models.DashboardProvisioning{} return nil }) @@ -241,8 +241,12 @@ type Result struct { } func setupDeleteHandlers(provisioned bool) *Result { - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = provisioned + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + if provisioned { + cmd.Result = &models.DashboardProvisioning{} + } else { + cmd.Result = nil + } return nil }) diff --git a/pkg/services/dashboards/folder_service_test.go b/pkg/services/dashboards/folder_service_test.go index 4c9cecd3352c8..bdf7556413ff9 100644 --- a/pkg/services/dashboards/folder_service_test.go +++ b/pkg/services/dashboards/folder_service_test.go @@ -112,8 +112,9 @@ func TestFolderService(t *testing.T) { provisioningValidated := false - bus.AddHandler("test", func(query *models.IsDashboardProvisionedQuery) error { + bus.AddHandler("test", func(query *models.GetProvisionedDashboardDataByIdQuery) error { provisioningValidated = true + query.Result = nil return nil }) diff --git a/pkg/services/notifications/codes.go b/pkg/services/notifications/codes.go index 4dbe76c1cad57..6382b609036b6 100644 --- a/pkg/services/notifications/codes.go +++ b/pkg/services/notifications/codes.go @@ -7,6 +7,7 @@ import ( "time" "github.com/Unknwon/com" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) diff --git a/pkg/services/provisioning/dashboards/dashboard.go b/pkg/services/provisioning/dashboards/dashboard.go index b7e5539c3b027..fd3a824420c6b 100644 --- a/pkg/services/provisioning/dashboards/dashboard.go +++ b/pkg/services/provisioning/dashboards/dashboard.go @@ -7,18 +7,11 @@ import ( "github.com/pkg/errors" ) -type DashboardProvisioner interface { - Provision() error - PollChanges(ctx context.Context) -} - type DashboardProvisionerImpl struct { log log.Logger fileReaders []*fileReader } -type DashboardProvisionerFactory func(string) (DashboardProvisioner, error) - func NewDashboardProvisionerImpl(configDirectory string) (*DashboardProvisionerImpl, error) { logger := log.New("provisioning.dashboard") cfgReader := &configReader{path: configDirectory, log: logger} @@ -61,6 +54,17 @@ func (provider *DashboardProvisionerImpl) PollChanges(ctx context.Context) { } } +// GetProvisionerResolvedPath returns resolved path for the specified provisioner name. Can be used to generate +// relative path to provisioning file from it's external_id. +func (provider *DashboardProvisionerImpl) GetProvisionerResolvedPath(name string) string { + for _, reader := range provider.fileReaders { + if reader.Cfg.Name == name { + return reader.resolvedPath() + } + } + return "" +} + func getFileReaders(configs []*DashboardsAsConfig, logger log.Logger) ([]*fileReader, error) { var readers []*fileReader diff --git a/pkg/services/provisioning/dashboards/dashboard_mock.go b/pkg/services/provisioning/dashboards/dashboard_mock.go index 5cdaab9be70dd..303338106e635 100644 --- a/pkg/services/provisioning/dashboards/dashboard_mock.go +++ b/pkg/services/provisioning/dashboards/dashboard_mock.go @@ -3,14 +3,16 @@ package dashboards import "context" type Calls struct { - Provision []interface{} - PollChanges []interface{} + Provision []interface{} + PollChanges []interface{} + GetProvisionerResolvedPath []interface{} } type DashboardProvisionerMock struct { - Calls *Calls - ProvisionFunc func() error - PollChangesFunc func(ctx context.Context) + Calls *Calls + ProvisionFunc func() error + PollChangesFunc func(ctx context.Context) + GetProvisionerResolvedPathFunc func(name string) string } func NewDashboardProvisionerMock() *DashboardProvisionerMock { @@ -34,3 +36,12 @@ func (dpm *DashboardProvisionerMock) PollChanges(ctx context.Context) { dpm.PollChangesFunc(ctx) } } + +func (dpm *DashboardProvisionerMock) GetProvisionerResolvedPath(name string) string { + dpm.Calls.PollChanges = append(dpm.Calls.GetProvisionerResolvedPath, name) + if dpm.GetProvisionerResolvedPathFunc != nil { + return dpm.GetProvisionerResolvedPathFunc(name) + } else { + return "" + } +} diff --git a/pkg/services/provisioning/dashboards/file_reader.go b/pkg/services/provisioning/dashboards/file_reader.go index 62709ce6ba9a8..96da9c8f6dfbf 100644 --- a/pkg/services/provisioning/dashboards/file_reader.go +++ b/pkg/services/provisioning/dashboards/file_reader.go @@ -70,7 +70,7 @@ func (fr *fileReader) pollChanges(ctx context.Context) { // to the database. func (fr *fileReader) startWalkingDisk() error { fr.log.Debug("Start walking disk", "path", fr.Path) - resolvedPath := fr.resolvePath(fr.Path) + resolvedPath := fr.resolvedPath() if _, err := os.Stat(resolvedPath); err != nil { if os.IsNotExist(err) { return err @@ -329,24 +329,23 @@ func (fr *fileReader) readDashboardFromFile(path string, lastModified time.Time, }, nil } -func (fr *fileReader) resolvePath(path string) string { - if _, err := os.Stat(path); os.IsNotExist(err) { +func (fr *fileReader) resolvedPath() string { + if _, err := os.Stat(fr.Path); os.IsNotExist(err) { fr.log.Error("Cannot read directory", "error", err) } - copy := path - path, err := filepath.Abs(path) + path, err := filepath.Abs(fr.Path) if err != nil { - fr.log.Error("Could not create absolute path", "path", copy, "error", err) + fr.log.Error("Could not create absolute path", "path", fr.Path, "error", err) } path, err = filepath.EvalSymlinks(path) if err != nil { - fr.log.Error("Failed to read content of symlinked path", "path", copy, "error", err) + fr.log.Error("Failed to read content of symlinked path", "path", fr.Path, "error", err) } if path == "" { - path = copy + path = fr.Path fr.log.Info("falling back to original path due to EvalSymlink/Abs failure") } return path diff --git a/pkg/services/provisioning/dashboards/file_reader_linux_test.go b/pkg/services/provisioning/dashboards/file_reader_linux_test.go index 77f488ebcfb6c..d62a59a4f4c74 100644 --- a/pkg/services/provisioning/dashboards/file_reader_linux_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_linux_test.go @@ -33,7 +33,7 @@ func TestProvsionedSymlinkedFolder(t *testing.T) { t.Errorf("expected err to be nil") } - resolvedPath := reader.resolvePath(reader.Path) + resolvedPath := reader.resolvedPath() if resolvedPath != want { t.Errorf("got %s want %s", resolvedPath, want) } diff --git a/pkg/services/provisioning/dashboards/file_reader_test.go b/pkg/services/provisioning/dashboards/file_reader_test.go index 8c0a04a808ae5..efc6052cbe799 100644 --- a/pkg/services/provisioning/dashboards/file_reader_test.go +++ b/pkg/services/provisioning/dashboards/file_reader_test.go @@ -70,7 +70,7 @@ func TestCreatingNewDashboardFileReader(t *testing.T) { reader, err := NewDashboardFileReader(cfg, log.New("test-logger")) So(err, ShouldBeNil) - resolvedPath := reader.resolvePath(reader.Path) + resolvedPath := reader.resolvedPath() So(filepath.IsAbs(resolvedPath), ShouldBeTrue) }) }) @@ -435,6 +435,10 @@ func (s *fakeDashboardProvisioningService) DeleteProvisionedDashboard(dashboardI return nil } +func (s *fakeDashboardProvisioningService) GetProvisionedDashboardDataByDashboardId(dashboardId int64) (*models.DashboardProvisioning, error) { + return nil, nil +} + func mockGetDashboardQuery(cmd *models.GetDashboardQuery) error { for _, d := range fakeService.getDashboard { if d.Slug == cmd.Slug { diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index 21f619776207f..29f2d13916433 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -2,11 +2,12 @@ package provisioning import ( "context" - "github.com/grafana/grafana/pkg/log" - "github.com/pkg/errors" "path" "sync" + "github.com/grafana/grafana/pkg/log" + "github.com/pkg/errors" + "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/services/provisioning/datasources" @@ -14,9 +15,17 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +type DashboardProvisioner interface { + Provision() error + PollChanges(ctx context.Context) + GetProvisionerResolvedPath(name string) string +} + +type DashboardProvisionerFactory func(string) (DashboardProvisioner, error) + func init() { registry.RegisterService(NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { + func(path string) (DashboardProvisioner, error) { return dashboards.NewDashboardProvisionerImpl(path) }, notifiers.Provision, @@ -24,14 +33,8 @@ func init() { )) } -type ProvisioningService interface { - ProvisionDatasources() error - ProvisionNotifications() error - ProvisionDashboards() error -} - func NewProvisioningServiceImpl( - newDashboardProvisioner dashboards.DashboardProvisionerFactory, + newDashboardProvisioner DashboardProvisionerFactory, provisionNotifiers func(string) error, provisionDatasources func(string) error, ) *provisioningServiceImpl { @@ -47,8 +50,8 @@ type provisioningServiceImpl struct { Cfg *setting.Cfg `inject:""` log log.Logger pollingCtxCancel context.CancelFunc - newDashboardProvisioner dashboards.DashboardProvisionerFactory - dashboardProvisioner dashboards.DashboardProvisioner + newDashboardProvisioner DashboardProvisionerFactory + dashboardProvisioner DashboardProvisioner provisionNotifiers func(string) error provisionDatasources func(string) error mutex sync.Mutex @@ -78,7 +81,9 @@ func (ps *provisioningServiceImpl) Run(ctx context.Context) error { // Wait for unlock. This is tied to new dashboardProvisioner to be instantiated before we start polling. ps.mutex.Lock() - pollingContext, cancelFun := context.WithCancel(ctx) + // Using background here because otherwise if root context was canceled the select later on would + // non-deterministically take one of the route possibly going into one polling loop before exiting. + pollingContext, cancelFun := context.WithCancel(context.Background()) ps.pollingCtxCancel = cancelFun ps.dashboardProvisioner.PollChanges(pollingContext) ps.mutex.Unlock() @@ -88,7 +93,8 @@ func (ps *provisioningServiceImpl) Run(ctx context.Context) error { // Polling was canceled. continue case <-ctx.Done(): - // Root server context was cancelled so just leave. + // Root server context was cancelled so cancel polling and leave. + ps.cancelPolling() return ctx.Err() } } @@ -127,6 +133,10 @@ func (ps *provisioningServiceImpl) ProvisionDashboards() error { return nil } +func (ps *provisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string { + return ps.dashboardProvisioner.GetProvisionerResolvedPath(name) +} + func (ps *provisioningServiceImpl) cancelPolling() { if ps.pollingCtxCancel != nil { ps.log.Debug("Stop polling for dashboard changes") diff --git a/pkg/services/provisioning/provisioning_mock.go b/pkg/services/provisioning/provisioning_mock.go new file mode 100644 index 0000000000000..7977e59b2e472 --- /dev/null +++ b/pkg/services/provisioning/provisioning_mock.go @@ -0,0 +1,58 @@ +package provisioning + +type Calls struct { + ProvisionDatasources []interface{} + ProvisionNotifications []interface{} + ProvisionDashboards []interface{} + GetDashboardProvisionerResolvedPath []interface{} +} + +type ProvisioningServiceMock struct { + Calls *Calls + ProvisionDatasourcesFunc func() error + ProvisionNotificationsFunc func() error + ProvisionDashboardsFunc func() error + GetDashboardProvisionerResolvedPathFunc func(name string) string +} + +func NewProvisioningServiceMock() *ProvisioningServiceMock { + return &ProvisioningServiceMock{ + Calls: &Calls{}, + } +} + +func (mock *ProvisioningServiceMock) ProvisionDatasources() error { + mock.Calls.ProvisionDatasources = append(mock.Calls.ProvisionDatasources, nil) + if mock.ProvisionDatasourcesFunc != nil { + return mock.ProvisionDatasourcesFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) ProvisionNotifications() error { + mock.Calls.ProvisionNotifications = append(mock.Calls.ProvisionNotifications, nil) + if mock.ProvisionNotificationsFunc != nil { + return mock.ProvisionNotificationsFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) ProvisionDashboards() error { + mock.Calls.ProvisionDashboards = append(mock.Calls.ProvisionDashboards, nil) + if mock.ProvisionDashboardsFunc != nil { + return mock.ProvisionDashboardsFunc() + } else { + return nil + } +} + +func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string { + mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name) + if mock.GetDashboardProvisionerResolvedPathFunc != nil { + return mock.GetDashboardProvisionerResolvedPathFunc(name) + } else { + return "" + } +} diff --git a/pkg/services/provisioning/provisioning_test.go b/pkg/services/provisioning/provisioning_test.go index 73f207122f7fd..119fa7261caa9 100644 --- a/pkg/services/provisioning/provisioning_test.go +++ b/pkg/services/provisioning/provisioning_test.go @@ -3,88 +3,130 @@ package provisioning import ( "context" "errors" + "testing" + "time" + "github.com/grafana/grafana/pkg/services/provisioning/dashboards" "github.com/grafana/grafana/pkg/setting" "github.com/stretchr/testify/assert" - "testing" - "time" ) func TestProvisioningServiceImpl(t *testing.T) { t.Run("Restart dashboard provisioning and stop service", func(t *testing.T) { - service, mock := setup() - ctx, cancel := context.WithCancel(context.Background()) - var serviceRunning bool - var serviceError error - - err := service.ProvisionDashboards() + serviceTest := setup() + err := serviceTest.service.ProvisionDashboards() assert.Nil(t, err) - go func() { - serviceRunning = true - serviceError = service.Run(ctx) - serviceRunning = false - }() - time.Sleep(time.Millisecond) - assert.Equal(t, 1, len(mock.Calls.PollChanges), "PollChanges should have been called") + serviceTest.startService() + serviceTest.waitForPollChanges() - err = service.ProvisionDashboards() + assert.Equal(t, 1, len(serviceTest.mock.Calls.PollChanges), "PollChanges should have been called") + + err = serviceTest.service.ProvisionDashboards() assert.Nil(t, err) - time.Sleep(time.Millisecond) - assert.Equal(t, 2, len(mock.Calls.PollChanges), "PollChanges should have been called 2 times") - pollingCtx := mock.Calls.PollChanges[0].(context.Context) + serviceTest.waitForPollChanges() + assert.Equal(t, 2, len(serviceTest.mock.Calls.PollChanges), "PollChanges should have been called 2 times") + + pollingCtx := serviceTest.mock.Calls.PollChanges[0].(context.Context) assert.Equal(t, context.Canceled, pollingCtx.Err(), "Polling context from first call should have been cancelled") - assert.True(t, serviceRunning, "Service should be still running") + assert.True(t, serviceTest.serviceRunning, "Service should be still running") // Cancelling the root context and stopping the service - cancel() - time.Sleep(time.Millisecond) + serviceTest.cancel() + serviceTest.waitForStop() - assert.False(t, serviceRunning, "Service should not be running") - assert.Equal(t, context.Canceled, serviceError, "Service should have returned canceled error") + assert.False(t, serviceTest.serviceRunning, "Service should not be running") + assert.Equal(t, context.Canceled, serviceTest.serviceError, "Service should have returned canceled error") }) t.Run("Failed reloading does not stop polling with old provisioned", func(t *testing.T) { - service, mock := setup() - ctx, cancel := context.WithCancel(context.Background()) - var serviceRunning bool - - err := service.ProvisionDashboards() + serviceTest := setup() + err := serviceTest.service.ProvisionDashboards() assert.Nil(t, err) - go func() { - serviceRunning = true - _ = service.Run(ctx) - serviceRunning = false - }() - time.Sleep(time.Millisecond) - assert.Equal(t, 1, len(mock.Calls.PollChanges), "PollChanges should have been called") + serviceTest.startService() + serviceTest.waitForPollChanges() + assert.Equal(t, 1, len(serviceTest.mock.Calls.PollChanges), "PollChanges should have been called") - mock.ProvisionFunc = func() error { + serviceTest.mock.ProvisionFunc = func() error { return errors.New("Test error") } - err = service.ProvisionDashboards() + err = serviceTest.service.ProvisionDashboards() assert.NotNil(t, err) - time.Sleep(time.Millisecond) + serviceTest.waitForPollChanges() + // This should have been called with the old provisioner, after the last one failed. - assert.Equal(t, 2, len(mock.Calls.PollChanges), "PollChanges should have been called 2 times") - assert.True(t, serviceRunning, "Service should be still running") + assert.Equal(t, 2, len(serviceTest.mock.Calls.PollChanges), "PollChanges should have been called 2 times") + assert.True(t, serviceTest.serviceRunning, "Service should be still running") // Cancelling the root context and stopping the service - cancel() - + serviceTest.cancel() }) } -func setup() (*provisioningServiceImpl, *dashboards.DashboardProvisionerMock) { - dashMock := dashboards.NewDashboardProvisionerMock() - service := NewProvisioningServiceImpl( - func(path string) (dashboards.DashboardProvisioner, error) { - return dashMock, nil +type serviceTestStruct struct { + waitForPollChanges func() + waitForStop func() + waitTimeout time.Duration + + serviceRunning bool + serviceError error + + startService func() + cancel func() + + mock *dashboards.DashboardProvisionerMock + service *provisioningServiceImpl +} + +func setup() *serviceTestStruct { + serviceTest := &serviceTestStruct{} + serviceTest.waitTimeout = time.Second + + pollChangesChannel := make(chan context.Context) + serviceStopped := make(chan interface{}) + + serviceTest.mock = dashboards.NewDashboardProvisionerMock() + serviceTest.mock.PollChangesFunc = func(ctx context.Context) { + pollChangesChannel <- ctx + } + + serviceTest.service = NewProvisioningServiceImpl( + func(path string) (DashboardProvisioner, error) { + return serviceTest.mock, nil }, nil, nil, ) - service.Cfg = setting.NewCfg() - return service, dashMock + serviceTest.service.Cfg = setting.NewCfg() + + ctx, cancel := context.WithCancel(context.Background()) + serviceTest.cancel = cancel + + serviceTest.startService = func() { + go func() { + serviceTest.serviceRunning = true + serviceTest.serviceError = serviceTest.service.Run(ctx) + serviceTest.serviceRunning = false + serviceStopped <- true + }() + } + + serviceTest.waitForPollChanges = func() { + timeoutChan := time.After(serviceTest.waitTimeout) + select { + case <-pollChangesChannel: + case <-timeoutChan: + } + } + + serviceTest.waitForStop = func() { + timeoutChan := time.After(serviceTest.waitTimeout) + select { + case <-serviceStopped: + case <-timeoutChan: + } + } + + return serviceTest } diff --git a/pkg/services/quota/quota.go b/pkg/services/quota/quota.go index b65ad9326991a..7e1e62fc52ace 100644 --- a/pkg/services/quota/quota.go +++ b/pkg/services/quota/quota.go @@ -43,7 +43,7 @@ func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, erro } if target == "session" { - usedSessions, err := qs.AuthTokenService.ActiveTokenCount() + usedSessions, err := qs.AuthTokenService.ActiveTokenCount(c.Req.Context()) if err != nil { return false, err } diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go index 29c2f39fd7745..35389fe7918e1 100644 --- a/pkg/services/rendering/phantomjs.go +++ b/pkg/services/rendering/phantomjs.go @@ -42,7 +42,8 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( cmdArgs := []string{ "--ignore-ssl-errors=true", - "--web-security=false", + "--web-security=true", + "--local-url-access=false", phantomDebugArg, scriptPath, fmt.Sprintf("url=%v", url), diff --git a/pkg/services/sqlstore/dashboard_provisioning.go b/pkg/services/sqlstore/dashboard_provisioning.go index b6c20682faee1..48238477ce990 100644 --- a/pkg/services/sqlstore/dashboard_provisioning.go +++ b/pkg/services/sqlstore/dashboard_provisioning.go @@ -19,16 +19,16 @@ type DashboardExtras struct { Value string } -func GetProvisionedDataByDashboardId(cmd *models.IsDashboardProvisionedQuery) error { +func GetProvisionedDataByDashboardId(cmd *models.GetProvisionedDashboardDataByIdQuery) error { result := &models.DashboardProvisioning{} exist, err := x.Where("dashboard_id = ?", cmd.DashboardId).Get(result) if err != nil { return err } - - cmd.Result = exist - + if exist { + cmd.Result = result + } return nil } diff --git a/pkg/services/sqlstore/dashboard_provisioning_test.go b/pkg/services/sqlstore/dashboard_provisioning_test.go index 82ac294349c1b..b58829c92f8ad 100644 --- a/pkg/services/sqlstore/dashboard_provisioning_test.go +++ b/pkg/services/sqlstore/dashboard_provisioning_test.go @@ -65,20 +65,20 @@ func TestDashboardProvisioningTest(t *testing.T) { }) Convey("Can query for one provisioned dashboard", func() { - query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} err := GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeTrue) + So(query.Result, ShouldNotBeNil) }) Convey("Can query for none provisioned dashboard", func() { - query := &models.IsDashboardProvisionedQuery{DashboardId: 3000} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: 3000} err := GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) Convey("Deleting folder should delete provision meta data", func() { @@ -89,11 +89,11 @@ func TestDashboardProvisioningTest(t *testing.T) { So(DeleteDashboard(deleteCmd), ShouldBeNil) - query := &models.IsDashboardProvisionedQuery{DashboardId: cmd.Result.Id} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: cmd.Result.Id} err = GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) Convey("UnprovisionDashboard should delete provisioning metadata", func() { @@ -103,11 +103,11 @@ func TestDashboardProvisioningTest(t *testing.T) { So(UnprovisionDashboard(unprovisionCmd), ShouldBeNil) - query := &models.IsDashboardProvisionedQuery{DashboardId: dashId} + query := &models.GetProvisionedDashboardDataByIdQuery{DashboardId: dashId} err = GetProvisionedDataByDashboardId(query) So(err, ShouldBeNil) - So(query.Result, ShouldBeFalse) + So(query.Result, ShouldBeNil) }) }) }) diff --git a/pkg/services/sqlstore/dashboard_service_integration_test.go b/pkg/services/sqlstore/dashboard_service_integration_test.go index a4e76aca34068..82e0d21213076 100644 --- a/pkg/services/sqlstore/dashboard_service_integration_test.go +++ b/pkg/services/sqlstore/dashboard_service_integration_test.go @@ -27,8 +27,8 @@ func TestIntegratedDashboardService(t *testing.T) { return nil }) - bus.AddHandler("test", func(cmd *models.IsDashboardProvisionedQuery) error { - cmd.Result = false + bus.AddHandler("test", func(cmd *models.GetProvisionedDashboardDataByIdQuery) error { + cmd.Result = nil return nil }) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 6d407941608cb..4f017591f0b23 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -918,8 +918,10 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { return err } - AlertingEvaluationTimeout = alerting.Key("evaluation_timeout_seconds").MustDuration(time.Second * 30) - AlertingNotificationTimeout = alerting.Key("notification_timeout_seconds").MustDuration(time.Second * 30) + evaluationTimeoutSeconds := alerting.Key("evaluation_timeout_seconds").MustInt64(30) + AlertingEvaluationTimeout = time.Second * time.Duration(evaluationTimeoutSeconds) + notificationTimeoutSeconds := alerting.Key("notification_timeout_seconds").MustInt64(30) + AlertingNotificationTimeout = time.Second * time.Duration(notificationTimeoutSeconds) AlertingMaxAttempts = alerting.Key("max_attempts").MustInt(3) explore := iniFile.Section("explore") diff --git a/public/app/core/actions/navModel.ts b/public/app/core/actions/navModel.ts index a40a0e880ee05..1f4755e16edf8 100644 --- a/public/app/core/actions/navModel.ts +++ b/public/app/core/actions/navModel.ts @@ -1,4 +1,4 @@ -import { NavModelItem } from '../../types'; +import { NavModelItem } from '@grafana/ui'; export enum ActionTypes { UpdateNavIndex = 'UPDATE_NAV_INDEX', diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index 1803317a8daf8..04986011fd4da 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -1,14 +1,13 @@ // Libraries import React, { Component } from 'react'; import config from 'app/core/config'; -import { NavModel } from 'app/types'; import { getTitleFromNavModel } from 'app/core/selectors/navModel'; // Components import PageHeader from '../PageHeader/PageHeader'; import Footer from '../Footer/Footer'; import PageContents from './PageContents'; -import { CustomScrollbar } from '@grafana/ui'; +import { CustomScrollbar, NavModel } from '@grafana/ui'; import { isEqual } from 'lodash'; interface Props { diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 623070651d1d4..d0cbf0f8c13af 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,7 +1,7 @@ import React, { FormEvent } from 'react'; -import { NavModel, NavModelItem } from 'app/types'; import classNames from 'classnames'; import appEvents from 'app/core/app_events'; +import { NavModel, NavModelItem, NavModelBreadcrumb } from '@grafana/ui'; export interface Props { model: NavModel; @@ -89,7 +89,7 @@ export default class PageHeader extends React.Component { return true; } - renderTitle(title: string, breadcrumbs: any[]) { + renderTitle(title: string, breadcrumbs: NavModelBreadcrumb[]) { if (!title && (!breadcrumbs || breadcrumbs.length === 0)) { return null; } @@ -99,16 +99,15 @@ export default class PageHeader extends React.Component { } const breadcrumbsResult = []; - for (let i = 0; i < breadcrumbs.length; i++) { - const bc = breadcrumbs[i]; + for (const bc of breadcrumbs) { if (bc.url) { breadcrumbsResult.push( - + {bc.title} ); } else { - breadcrumbsResult.push( / {bc.title}); + breadcrumbsResult.push( / {bc.title}); } } breadcrumbsResult.push( / {title}); @@ -116,7 +115,7 @@ export default class PageHeader extends React.Component { return

{breadcrumbsResult}

; } - renderHeaderTitle(main) { + renderHeaderTitle(main: NavModelItem) { return (
@@ -127,12 +126,6 @@ export default class PageHeader extends React.Component {
{this.renderTitle(main.text, main.breadcrumbs)} {main.subTitle &&
{main.subTitle}
} - {main.subType && ( -
- - {main.subType.text} -
- )}
); diff --git a/public/app/core/components/navbar/navbar.ts b/public/app/core/components/navbar/navbar.ts index db0924738ed9c..f4de87451728d 100644 --- a/public/app/core/components/navbar/navbar.ts +++ b/public/app/core/components/navbar/navbar.ts @@ -1,6 +1,6 @@ import coreModule from '../../core_module'; -import { NavModel } from '../../nav_model_srv'; import appEvents from 'app/core/app_events'; +import { NavModel } from '@grafana/ui'; export class NavbarCtrl { model: NavModel; diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 4d83cb8f5b17f..8d762514e003c 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,6 +1,5 @@ import _ from 'lodash'; -import { PanelPluginMeta } from 'app/types/plugins'; -import { GrafanaTheme, getTheme, GrafanaThemeType, DataSourceInstanceSettings } from '@grafana/ui'; +import { GrafanaTheme, getTheme, GrafanaThemeType, PanelPluginMeta, DataSourceInstanceSettings } from '@grafana/ui'; export interface BuildInfo { version: string; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 80987b8fc88e2..c7f03781710cb 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -40,7 +40,7 @@ import { contextSrv } from './services/context_srv'; import { KeybindingSrv } from './services/keybindingSrv'; import { helpModal } from './components/help/help'; import { JsonExplorer } from './components/json_explorer/json_explorer'; -import { NavModelSrv, NavModel } from './nav_model_srv'; +import { NavModelSrv } from './nav_model_srv'; import { geminiScrollbar } from './components/scroll/scroll'; import { orgSwitcher } from './components/org_switcher'; import { profiler } from './profiler'; @@ -49,6 +49,7 @@ import { updateLegendValues } from './time_series2'; import TimeSeries from './time_series2'; import { searchResultsDirective } from './components/search/search_results'; import { manageDashboardsDirective } from './components/manage_dashboards/manage_dashboards'; +import { NavModel } from '@grafana/ui'; export { profiler, diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 9d1ca2f44b798..03420f8029949 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,7 +1,22 @@ import _ from 'lodash'; - -import { colors, TimeSeries, Labels, LogLevel } from '@grafana/ui'; +import moment from 'moment'; +import ansicolor from 'vendor/ansicolor/ansicolor'; + +import { + colors, + TimeSeries, + Labels, + LogLevel, + SeriesData, + findCommonLabels, + findUniqueLabels, + getLogLevel, + toLegacyResponseData, + FieldCache, + FieldType, +} from '@grafana/ui'; import { getThemeColor } from 'app/core/utils/colors'; +import { hasAnsiCodes } from 'app/core/utils/text'; export const LogLevelColor = { [LogLevel.critical]: colors[7], @@ -23,7 +38,6 @@ export interface LogRowModel { duplicates?: number; entry: string; hasAnsi: boolean; - key: string; // timestamp + labels labels: Labels; logLevel: LogLevel; raw: string; @@ -56,27 +70,11 @@ export interface LogsMetaItem { export interface LogsModel { hasUniqueLabels: boolean; - id: string; // Identify one logs result from another meta?: LogsMetaItem[]; rows: LogRowModel[]; series?: TimeSeries[]; } -export interface LogsStream { - labels: string; - entries: LogsStreamEntry[]; - search?: string; - parsedLabels?: Labels; - uniqueLabels?: Labels; -} - -export interface LogsStreamEntry { - line: string; - ts: string; - // Legacy, was renamed to ts - timestamp?: string; -} - export enum LogsDedupDescription { none = 'No de-duplication', exact = 'De-duplication of successive lines that are identical, ignoring ISO datetimes.', @@ -326,3 +324,136 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time }; }); } + +function isLogsData(series: SeriesData) { + return series.fields.some(f => f.type === FieldType.time) && series.fields.some(f => f.type === FieldType.string); +} + +export function seriesDataToLogsModel(seriesData: SeriesData[], intervalMs: number): LogsModel { + const metricSeries: SeriesData[] = []; + const logSeries: SeriesData[] = []; + + for (const series of seriesData) { + if (isLogsData(series)) { + logSeries.push(series); + continue; + } + + metricSeries.push(series); + } + + const logsModel = logSeriesToLogsModel(logSeries); + if (logsModel) { + if (metricSeries.length === 0) { + logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs); + } else { + logsModel.series = []; + for (const series of metricSeries) { + logsModel.series.push(toLegacyResponseData(series) as TimeSeries); + } + } + + return logsModel; + } + + return undefined; +} + +export function logSeriesToLogsModel(logSeries: SeriesData[]): LogsModel { + if (logSeries.length === 0) { + return undefined; + } + + const allLabels: Labels[] = []; + for (let n = 0; n < logSeries.length; n++) { + const series = logSeries[n]; + if (series.labels) { + allLabels.push(series.labels); + } + } + + let commonLabels: Labels = {}; + if (allLabels.length > 0) { + commonLabels = findCommonLabels(allLabels); + } + + const rows: LogRowModel[] = []; + let hasUniqueLabels = false; + + for (let i = 0; i < logSeries.length; i++) { + const series = logSeries[i]; + const fieldCache = new FieldCache(series.fields); + const uniqueLabels = findUniqueLabels(series.labels, commonLabels); + if (Object.keys(uniqueLabels).length > 0) { + hasUniqueLabels = true; + } + + for (let j = 0; j < series.rows.length; j++) { + rows.push(processLogSeriesRow(series, fieldCache, j, uniqueLabels)); + } + } + + const sortedRows = rows.sort((a, b) => { + return a.timestamp > b.timestamp ? -1 : 1; + }); + + // Meta data to display in status + const meta: LogsMetaItem[] = []; + if (_.size(commonLabels) > 0) { + meta.push({ + label: 'Common labels', + value: commonLabels, + kind: LogsMetaKind.LabelsMap, + }); + } + + const limits = logSeries.filter(series => series.meta && series.meta.limit); + + if (limits.length > 0) { + meta.push({ + label: 'Limit', + value: `${limits[0].meta.limit} (${sortedRows.length} returned)`, + kind: LogsMetaKind.String, + }); + } + + return { + hasUniqueLabels, + meta, + rows: sortedRows, + }; +} + +export function processLogSeriesRow( + series: SeriesData, + fieldCache: FieldCache, + rowIndex: number, + uniqueLabels: Labels +): LogRowModel { + const row = series.rows[rowIndex]; + const timeFieldIndex = fieldCache.getFirstFieldOfType(FieldType.time).index; + const ts = row[timeFieldIndex]; + const stringFieldIndex = fieldCache.getFirstFieldOfType(FieldType.string).index; + const message = row[stringFieldIndex]; + const time = moment(ts); + const timeEpochMs = time.valueOf(); + const timeFromNow = time.fromNow(); + const timeLocal = time.format('YYYY-MM-DD HH:mm:ss'); + const logLevel = getLogLevel(message); + const hasAnsi = hasAnsiCodes(message); + const search = series.meta && series.meta.search ? series.meta.search : ''; + + return { + logLevel, + timeFromNow, + timeEpochMs, + timeLocal, + uniqueLabels, + hasAnsi, + entry: hasAnsi ? ansicolor.strip(message) : message, + raw: message, + labels: series.labels, + searchWords: search ? [search] : [], + timestamp: ts, + }; +} diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index 661cdbe77a3e4..a394511aa2c4d 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -1,29 +1,7 @@ import coreModule from 'app/core/core_module'; import config from 'app/core/config'; import _ from 'lodash'; - -export interface NavModelItem { - text: string; - url: string; - icon?: string; - img?: string; - id: string; - active?: boolean; - hideFromTabs?: boolean; - divider?: boolean; - children: NavModelItem[]; - target?: string; -} - -export class NavModel { - breadcrumbs: NavModelItem[]; - main: NavModelItem; - node: NavModelItem; - - constructor() { - this.breadcrumbs = []; - } -} +import { NavModel } from '@grafana/ui'; export class NavModelSrv { navItems: any; @@ -39,7 +17,9 @@ export class NavModelSrv { getNav(...args) { let children = this.navItems; - const nav = new NavModel(); + const nav = { + breadcrumbs: [], + } as NavModel; for (const id of args) { // if its a number then it's the index to use for main diff --git a/public/app/core/reducers/navModel.ts b/public/app/core/reducers/navModel.ts index 942739e45e6f0..f75b701a9ff19 100644 --- a/public/app/core/reducers/navModel.ts +++ b/public/app/core/reducers/navModel.ts @@ -1,5 +1,5 @@ import { Action, ActionTypes } from 'app/core/actions/navModel'; -import { NavIndex, NavModelItem } from 'app/types'; +import { NavIndex, NavModelItem } from '@grafana/ui'; import config from 'app/core/config'; export function buildInitialState(): NavIndex { diff --git a/public/app/core/selectors/navModel.ts b/public/app/core/selectors/navModel.ts index 849ee364e25cb..66a7011389c7c 100644 --- a/public/app/core/selectors/navModel.ts +++ b/public/app/core/selectors/navModel.ts @@ -1,4 +1,4 @@ -import { NavModel, NavModelItem, NavIndex } from 'app/types'; +import { NavModel, NavModelItem, NavIndex } from '@grafana/ui'; function getNotFoundModel(): NavModel { const node: NavModelItem = { diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts index dcd1e48333071..807605965ef95 100644 --- a/public/app/core/specs/logs_model.test.ts +++ b/public/app/core/specs/logs_model.test.ts @@ -6,7 +6,10 @@ import { LogsDedupStrategy, LogsModel, LogsParsers, + seriesDataToLogsModel, + LogsMetaKind, } from '../logs_model'; +import { SeriesData, FieldType } from '@grafana/ui'; describe('dedupLogRows()', () => { test('should return rows as is when dedup is set to none', () => { @@ -329,3 +332,216 @@ describe('LogsParsers', () => { }); }); }); + +describe('seriesDataToLogsModel', () => { + it('given empty series should return undefined', () => { + expect(seriesDataToLogsModel([] as SeriesData[], 0)).toBeUndefined(); + }); + + it('given series without correct series name should not be processed', () => { + const series: SeriesData[] = [ + { + fields: [], + rows: [], + }, + ]; + expect(seriesDataToLogsModel(series, 0)).toBeUndefined(); + }); + + it('given series without a time field should not be processed', () => { + const series: SeriesData[] = [ + { + fields: [ + { + name: 'message', + type: FieldType.string, + }, + ], + rows: [], + }, + ]; + expect(seriesDataToLogsModel(series, 0)).toBeUndefined(); + }); + + it('given series without a string field should not be processed', () => { + const series: SeriesData[] = [ + { + fields: [ + { + name: 'time', + type: FieldType.time, + }, + ], + rows: [], + }, + ]; + expect(seriesDataToLogsModel(series, 0)).toBeUndefined(); + }); + + it('given one series should return expected logs model', () => { + const series: SeriesData[] = [ + { + labels: { + filename: '/var/log/grafana/grafana.log', + job: 'grafana', + }, + fields: [ + { + name: 'time', + type: FieldType.time, + }, + { + name: 'message', + type: FieldType.string, + }, + ], + rows: [ + [ + '2019-04-26T09:28:11.352440161Z', + 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server', + ], + [ + '2019-04-26T14:42:50.991981292Z', + 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7', + ], + ], + meta: { + limit: 1000, + }, + }, + ]; + const logsModel = seriesDataToLogsModel(series, 0); + expect(logsModel.hasUniqueLabels).toBeFalsy(); + expect(logsModel.rows).toHaveLength(2); + expect(logsModel.rows).toMatchObject([ + { + timestamp: '2019-04-26T14:42:50.991981292Z', + entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7', + labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + logLevel: 'error', + uniqueLabels: {}, + }, + { + timestamp: '2019-04-26T09:28:11.352440161Z', + entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server', + labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' }, + logLevel: 'info', + uniqueLabels: {}, + }, + ]); + + expect(logsModel.series).toHaveLength(2); + expect(logsModel.meta).toHaveLength(2); + expect(logsModel.meta[0]).toMatchObject({ + label: 'Common labels', + value: series[0].labels, + kind: LogsMetaKind.LabelsMap, + }); + expect(logsModel.meta[1]).toMatchObject({ + label: 'Limit', + value: `1000 (2 returned)`, + kind: LogsMetaKind.String, + }); + }); + + it('given one series without labels should return expected logs model', () => { + const series: SeriesData[] = [ + { + fields: [ + { + name: 'time', + type: FieldType.time, + }, + { + name: 'message', + type: FieldType.string, + }, + ], + rows: [['1970-01-01T00:00:01Z', 'WARN boooo']], + }, + ]; + const logsModel = seriesDataToLogsModel(series, 0); + expect(logsModel.rows).toHaveLength(1); + expect(logsModel.rows).toMatchObject([ + { + entry: 'WARN boooo', + labels: undefined, + logLevel: 'warning', + uniqueLabels: {}, + }, + ]); + }); + + it('given multiple series should return expected logs model', () => { + const series: SeriesData[] = [ + { + labels: { + foo: 'bar', + baz: '1', + }, + fields: [ + { + name: 'ts', + type: FieldType.time, + }, + { + name: 'line', + type: FieldType.string, + }, + ], + rows: [['1970-01-01T00:00:01Z', 'WARN boooo']], + }, + { + name: 'logs', + labels: { + foo: 'bar', + baz: '2', + }, + fields: [ + { + name: 'time', + type: FieldType.time, + }, + { + name: 'message', + type: FieldType.string, + }, + ], + rows: [['1970-01-01T00:00:00Z', 'INFO 1'], ['1970-01-01T00:00:02Z', 'INFO 2']], + }, + ]; + const logsModel = seriesDataToLogsModel(series, 0); + expect(logsModel.hasUniqueLabels).toBeTruthy(); + expect(logsModel.rows).toHaveLength(3); + expect(logsModel.rows).toMatchObject([ + { + entry: 'INFO 2', + labels: { foo: 'bar', baz: '2' }, + logLevel: 'info', + uniqueLabels: { baz: '2' }, + }, + { + entry: 'WARN boooo', + labels: { foo: 'bar', baz: '1' }, + logLevel: 'warning', + uniqueLabels: { baz: '1' }, + }, + { + entry: 'INFO 1', + labels: { foo: 'bar', baz: '2' }, + logLevel: 'info', + uniqueLabels: { baz: '2' }, + }, + ]); + + expect(logsModel.series).toHaveLength(2); + expect(logsModel.meta).toHaveLength(1); + expect(logsModel.meta[0]).toMatchObject({ + label: 'Common labels', + value: { + foo: 'bar', + }, + kind: LogsMetaKind.LabelsMap, + }); + }); +}); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index cfc53ae450bac..2994fac71be05 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,18 +1,27 @@ // Libraries import _ from 'lodash'; +import moment, { Moment } from 'moment'; // Services & Utils import * as dateMath from 'app/core/utils/datemath'; import { renderUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; import store from 'app/core/store'; -import { parse as parseDate } from 'app/core/utils/datemath'; -import { colors } from '@grafana/ui'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import { getNextRefIdChar } from './query'; // Types -import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui'; +import { + colors, + TimeRange, + RawTimeRange, + TimeZone, + IntervalValues, + DataQuery, + DataSourceApi, + toSeriesData, + guessFieldTypes, +} from '@grafana/ui'; import TimeSeries from 'app/core/time_series2'; import { ExploreUrlState, @@ -23,7 +32,7 @@ import { QueryOptions, ResultGetter, } from 'app/types/explore'; -import { LogsDedupStrategy } from 'app/core/logs_model'; +import { LogsDedupStrategy, seriesDataToLogsModel } from 'app/core/logs_model'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -104,7 +113,7 @@ export function buildQueryTransaction( rowIndex: number, resultType: ResultType, queryOptions: QueryOptions, - range: RawTimeRange, + range: TimeRange, queryIntervals: QueryIntervals, scanning: boolean ): QueryTransaction { @@ -131,12 +140,8 @@ export function buildQueryTransaction( intervalMs, panelId, targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. - range: { - from: dateMath.parse(range.from, false), - to: dateMath.parse(range.to, true), - raw: range, - }, - rangeRaw: range, + range, + rangeRaw: range.raw, scopedVars: { __interval: { text: interval, value: interval }, __interval_ms: { text: intervalMs, value: intervalMs }, @@ -298,15 +303,12 @@ export function calculateResultsFromQueryTransactions( .filter(qt => qt.resultType === 'Table' && qt.done && qt.result && qt.result.columns && qt.result.rows) .map(qt => qt.result) ); - const logsResult = - datasource && datasource.mergeStreams - ? datasource.mergeStreams( - _.flatten( - queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) - ), - graphInterval - ) - : undefined; + const logsResult = seriesDataToLogsModel( + _.flatten( + queryTransactions.filter(qt => qt.resultType === 'Logs' && qt.done && qt.result).map(qt => qt.result) + ).map(r => guessFieldTypes(toSeriesData(r))), + graphInterval + ); return { graphResult, @@ -315,17 +317,12 @@ export function calculateResultsFromQueryTransactions( }; } -export function getIntervals(range: RawTimeRange, lowLimit: string, resolution: number): IntervalValues { +export function getIntervals(range: TimeRange, lowLimit: string, resolution: number): IntervalValues { if (!resolution) { return { interval: '1s', intervalMs: 1000 }; } - const absoluteRange: RawTimeRange = { - from: parseDate(range.from, false), - to: parseDate(range.to, true), - }; - - return kbn.calculateInterval(absoluteRange, resolution, lowLimit); + return kbn.calculateInterval(range, resolution, lowLimit); } export const makeTimeSeriesList: ResultGetter = (dataList, transaction, allTransactions) => { @@ -395,3 +392,51 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourc return queryKeys; }; + +export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => { + return { + from: dateMath.parse(rawRange.from, false, timeZone.raw as any), + to: dateMath.parse(rawRange.to, true, timeZone.raw as any), + raw: rawRange, + }; +}; + +const parseRawTime = (value): Moment | string => { + if (value === null) { + return null; + } + + if (value.indexOf('now') !== -1) { + return value; + } + if (value.length === 8) { + return moment.utc(value, 'YYYYMMDD'); + } + if (value.length === 15) { + return moment.utc(value, 'YYYYMMDDTHHmmss'); + } + // Backward compatibility + if (value.length === 19) { + return moment.utc(value, 'YYYY-MM-DD HH:mm:ss'); + } + + if (!isNaN(value)) { + const epoch = parseInt(value, 10); + return moment.utc(epoch); + } + + return null; +}; + +export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): TimeRange => { + const raw = { + from: parseRawTime(range.from), + to: parseRawTime(range.to), + }; + + return { + from: dateMath.parse(raw.from, false, timeZone.raw as any), + to: dateMath.parse(raw.to, true, timeZone.raw as any), + raw, + }; +}; diff --git a/public/app/features/admin/ServerStats.tsx b/public/app/features/admin/ServerStats.tsx index 92e7da6e8311e..c5129d11fbe2d 100644 --- a/public/app/features/admin/ServerStats.tsx +++ b/public/app/features/admin/ServerStats.tsx @@ -1,10 +1,11 @@ import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -import { NavModel, StoreState } from 'app/types'; +import { StoreState } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { getServerStats, ServerStat } from './state/apis'; import Page from 'app/core/components/Page/Page'; +import { NavModel } from '@grafana/ui'; interface Props { navModel: NavModel; diff --git a/public/app/features/alerting/AlertRuleList.test.tsx b/public/app/features/alerting/AlertRuleList.test.tsx index 61b59cc852327..88720c775a960 100644 --- a/public/app/features/alerting/AlertRuleList.test.tsx +++ b/public/app/features/alerting/AlertRuleList.test.tsx @@ -1,10 +1,11 @@ import React from 'react'; import { shallow } from 'enzyme'; import { AlertRuleList, Props } from './AlertRuleList'; -import { AlertRule, NavModel } from '../../types'; +import { AlertRule } from '../../types'; import appEvents from '../../core/app_events'; import { mockActionCreator } from 'app/core/redux'; import { updateLocation } from 'app/core/actions'; +import { NavModel } from '@grafana/ui'; jest.mock('../../core/app_events', () => ({ emit: jest.fn(), diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index 9b4ceac3acd31..dcd31b71419c8 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -6,10 +6,11 @@ import AlertRuleItem from './AlertRuleItem'; import appEvents from 'app/core/app_events'; import { updateLocation } from 'app/core/actions'; import { getNavModel } from 'app/core/selectors/navModel'; -import { NavModel, StoreState, AlertRule } from 'app/types'; +import { StoreState, AlertRule } from 'app/types'; import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions'; import { getAlertRuleItems, getSearchQuery } from './state/selectors'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; +import { NavModel } from '@grafana/ui'; export interface Props { navModel: NavModel; diff --git a/public/app/features/api-keys/ApiKeysPage.test.tsx b/public/app/features/api-keys/ApiKeysPage.test.tsx index 23def68728e1c..37afa36b173ae 100644 --- a/public/app/features/api-keys/ApiKeysPage.test.tsx +++ b/public/app/features/api-keys/ApiKeysPage.test.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Props, ApiKeysPage } from './ApiKeysPage'; -import { NavModel, ApiKey } from 'app/types'; +import { ApiKey } from 'app/types'; import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock'; +import { NavModel } from '@grafana/ui'; const setup = (propOverrides?: object) => { const props: Props = { diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 9748a5727cfa6..7a058c1c25da9 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -2,7 +2,7 @@ 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 { ApiKey, NewApiKey, OrgRole } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { getApiKeys, getApiKeysCount } from './state/selectors'; import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions'; @@ -12,7 +12,7 @@ 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, Input } from '@grafana/ui'; +import { DeleteButton, Input, NavModel } from '@grafana/ui'; import { FilterInput } from 'app/core/components/FilterInput/FilterInput'; export interface Props { diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts index 76151e26c1584..00959e98a3c6f 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.test.ts @@ -9,7 +9,7 @@ import config from 'app/core/config'; import { DashboardExporter } from './DashboardExporter'; import { DashboardModel } from '../../state/DashboardModel'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { PanelPluginMeta } from 'app/types'; +import { PanelPluginMeta } from '@grafana/ui'; describe('given dashboard with repeated panels', () => { let dash: any, exported: any; diff --git a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts index 78d84350b572f..79cbe918434ba 100644 --- a/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts +++ b/public/app/features/dashboard/components/DashExportModal/DashboardExporter.ts @@ -4,7 +4,7 @@ import config from 'app/core/config'; import { DashboardModel } from '../../state/DashboardModel'; import DatasourceSrv from 'app/features/plugins/datasource_srv'; import { PanelModel } from 'app/features/dashboard/state'; -import { PanelPluginMeta } from 'app/types/plugins'; +import { PanelPluginMeta } from '@grafana/ui'; interface Input { name: string; diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index c0332e619507f..2fb9d76daa342 100755 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -192,6 +192,8 @@ export class SettingsCtrl { text2: ` See documentation for more information about provisioning. +
+ File path: ${this.dashboard.meta.provisionedExternalId} `, text2htmlBind: true, icon: 'fa-trash', diff --git a/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts index 851644d3f1490..ca85c962d1a41 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveProvisionedDashboardModalCtrl.ts @@ -1,6 +1,7 @@ import angular from 'angular'; import { saveAs } from 'file-saver'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from '../../state'; const template = `