diff --git a/.bra.toml b/.bra.toml index dcf316466d6..15961e1e3fd 100644 --- a/.bra.toml +++ b/.bra.toml @@ -9,7 +9,7 @@ watch_dirs = [ "$WORKDIR/public/views", "$WORKDIR/conf", ] -watch_exts = [".go", ".ini", ".toml"] +watch_exts = [".go", ".ini", ".toml", ".template.html"] build_delay = 1500 cmds = [ ["go", "run", "build.go", "-dev", "build-server"], diff --git a/.circleci/config.yml b/.circleci/config.yml index f351040fe2f..1e046aec34d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,9 +5,11 @@ aliases: ignore: /.*/ tags: only: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ - - &filter-not-release + - &filter-not-release-or-master tags: ignore: /^v[0-9]+(\.[0-9]+){2}(-.+|[^-.]*)$/ + branches: + ignore: master - &filter-only-master branches: only: master @@ -88,6 +90,9 @@ jobs: - run: name: run linters command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...' + - run: + name: run go vet + command: 'go vet ./pkg/...' test-frontend: docker: @@ -99,6 +104,7 @@ jobs: - run: name: yarn install command: 'yarn install --pure-lockfile --no-progress' + no_output_timeout: 15m - save_cache: key: dependency-cache-{{ checksum "yarn.lock" }} paths: @@ -141,6 +147,12 @@ jobs: - run: name: sign packages command: './scripts/build/sign_packages.sh' + - run: + name: verify signed packages + command: | + mkdir -p ~/.rpmdb/pubkeys + curl -s https://grafanarel.s3.amazonaws.com/RPM-GPG-KEY-grafana > ~/.rpmdb/pubkeys/grafana.key + ./scripts/build/verify_signed_packages.sh dist/*.rpm - run: name: sha-sum packages command: 'go run build.go sha-dist' @@ -153,8 +165,65 @@ jobs: - dist/grafana* - scripts/*.sh - scripts/publish - - store_artifacts: - path: dist + + build: + docker: + - image: grafana/build-container:1.0.0 + working_directory: /go/src/github.com/grafana/grafana + steps: + - checkout + - run: + name: prepare build tools + command: '/tmp/bootstrap.sh' + - run: + name: build and package grafana + command: './scripts/build/build.sh' + - run: + name: sign packages + command: './scripts/build/sign_packages.sh' + - run: + name: sha-sum packages + command: 'go run build.go sha-dist' + - persist_to_workspace: + root: . + paths: + - dist/grafana* + + grafana-docker-master: + docker: + - image: docker:stable-git + steps: + - checkout + - attach_workspace: + at: . + - setup_remote_docker + - run: docker info + - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "master-${CIRCLE_SHA1}" + + grafana-docker-pr: + docker: + - image: docker:stable-git + steps: + - checkout + - attach_workspace: + at: . + - setup_remote_docker + - run: docker info + - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: cd packaging/docker && ./build.sh "${CIRCLE_SHA1}" + + grafana-docker-release: + docker: + - image: docker:stable-git + steps: + - checkout + - attach_workspace: + at: . + - setup_remote_docker + - run: docker info + - run: cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + - run: cd packaging/docker && ./build-deploy.sh "${CIRCLE_TAG}" build-enterprise: docker: @@ -210,9 +279,6 @@ jobs: - run: name: Trigger Windows build command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master' - - run: - name: Trigger Docker build - command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)' - run: name: Publish to Grafana.com command: | @@ -234,30 +300,27 @@ jobs: - run: name: Trigger Windows build command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release' - - run: - name: Trigger Docker build - command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} ${CIRCLE_TAG}' workflows: version: 2 - test-and-build: + build-master: jobs: - build-all: - filters: *filter-not-release + filters: *filter-only-master - build-enterprise: filters: *filter-only-master - codespell: - filters: *filter-not-release + filters: *filter-only-master - gometalinter: - filters: *filter-not-release + filters: *filter-only-master - test-frontend: - filters: *filter-not-release + filters: *filter-only-master - test-backend: - filters: *filter-not-release + filters: *filter-only-master - mysql-integration-test: - filters: *filter-not-release + filters: *filter-only-master - postgres-integration-test: - filters: *filter-not-release + filters: *filter-only-master - deploy-master: requires: - build-all @@ -267,9 +330,17 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - filters: - branches: - only: master + filters: *filter-only-master + - grafana-docker-master: + requires: + - build-all + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-master - deploy-enterprise-master: requires: - build-all @@ -308,3 +379,40 @@ workflows: - mysql-integration-test - postgres-integration-test filters: *filter-only-release + - grafana-docker-release: + requires: + - build-all + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-only-release + + build-branches-and-prs: + jobs: + - build: + filters: *filter-not-release-or-master + - codespell: + filters: *filter-not-release-or-master + - gometalinter: + filters: *filter-not-release-or-master + - test-frontend: + filters: *filter-not-release-or-master + - test-backend: + filters: *filter-not-release-or-master + - mysql-integration-test: + filters: *filter-not-release-or-master + - postgres-integration-test: + filters: *filter-not-release-or-master + - grafana-docker-pr: + requires: + - build + - test-backend + - test-frontend + - codespell + - gometalinter + - mysql-integration-test + - postgres-integration-test + filters: *filter-not-release-or-master diff --git a/.dockerignore b/.dockerignore index e50dfd86aa3..c535fa427b5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,9 +3,12 @@ .git .gitignore .github +.vscode +bin data* dist docker +Dockerfile docs dump.rdb node_modules diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe0a1d6c548..769ba2a519b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -2,12 +2,12 @@ Follow the setup guide in README.md ### Rebuild frontend assets on source change ``` -grunt && grunt watch +yarn watch ``` ### Rerun tests on source change ``` -grunt karma:dev +yarn jest ``` ### Run tests for backend assets before commit @@ -17,6 +17,6 @@ test -z "$(gofmt -s -l . | grep -v -E 'vendor/(github.com|golang.org|gopkg.in)' ### Run tests for frontend assets before commit ``` -npm test +yarn test go test -v ./pkg/... ``` diff --git a/.gitignore b/.gitignore index 11df66360d9..bf97948d178 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ debug.test /examples/*/dist /packaging/**/*.rpm /packaging/**/*.deb +/packaging/**/*.tar.gz # Ignore OSX indexing .DS_Store @@ -70,4 +71,4 @@ debug.test /vendor/**/appengine* *.orig -/devenv/dashboards/bulk-testing/*.json +/devenv/bulk-dashboards/*.json diff --git a/.jscs.json b/.jscs.json deleted file mode 100644 index 8fdad332de5..00000000000 --- a/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 1d8fad63173..00000000000 --- a/.jshintrc +++ /dev/null @@ -1,37 +0,0 @@ -{ - "browser": true, - "esversion": 6, - "bitwise":false, - "curly": true, - "eqnull": true, - "strict": false, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": false, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "Promise": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ebb038546e..3befbc40eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,28 +1,87 @@ # 5.3.0 (unreleased) +* **OAuth**: Gitlab OAuth with support for filter by groups [#5623](https://github.com/grafana/grafana/issues/5623), thx [@BenoitKnecht](https://github.com/BenoitKnecht) * **Dataproxy**: Pass configured/auth headers to a Datasource [#10971](https://github.com/grafana/grafana/issues/10971), thx [@mrsiano](https://github.com/mrsiano) * **Cleanup**: Make temp file time to live configurable [#11607](https://github.com/grafana/grafana/issues/11607), thx [@xapon](https://github.com/xapon) +* **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622) +* **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda) +* **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos) +* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476) +* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi) +* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm) ### Minor * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley) * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248) +* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705) * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps) -* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) +* **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie) * **Variables**: Skip unneeded extra query request when de-selecting variable values used for repeated panels [#8186](https://github.com/grafana/grafana/issues/8186), thx [@mtanda](https://github.com/mtanda) +* **Variables**: Limit amount of queries executed when updating variable that other variable(s) are dependent on [#11890](https://github.com/grafana/grafana/issues/11890) +* **Variables**: Support query variable refresh when another variable referenced in `Regex` field change its value [#12952](https://github.com/grafana/grafana/issues/12952), thx [@franciscocpg](https://github.com/franciscocpg) +* **Variables**: Support variables in query variable `Custom all value` field [#12965](https://github.com/grafana/grafana/issues/12965), thx [@franciscocpg](https://github.com/franciscocpg) +* **Postgres/MySQL/MSSQL**: New $__unixEpochGroup and $__unixEpochGroupAlias macros [#12892](https://github.com/grafana/grafana/issues/12892), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Add previous fill mode to $__timeGroup macro which will fill in previously seen value when point is missing [#12756](https://github.com/grafana/grafana/issues/12756), thx [@svenklemm](https://github.com/svenklemm) * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Use metric column as prefix when returning multiple value columns [#12727](https://github.com/grafana/grafana/issues/12727), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: New $__timeGroupAlias macro. Postgres $__timeGroup no longer automatically adds time column alias [#12749](https://github.com/grafana/grafana/issues/12749), thx [@svenklemm](https://github.com/svenklemm) +* **Postgres/MySQL/MSSQL**: Escape single quotes in variables [#12785](https://github.com/grafana/grafana/issues/12785), thx [@eMerzh](https://github.com/eMerzh) * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley) +* **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan) * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber) * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane) +* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151) +* **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda) +* **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg) +* **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88) +* **Cloudwatch**: Added BurstBalance metric to list of AWS RDS metrics [#12561](https://github.com/grafana/grafana/pulls/12561), thx [@activeshadow](https://github.com/activeshadow) +* **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z) +* **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668) +* **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766) +om/grafana/grafana/issues/12668) +* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964) +* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2) +* **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731) +* **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj) +* **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty) +* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek) +* **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda) +* **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig) +* **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270) +* **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud) +* **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier) +* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927) +* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen) +* **Provisioning**: Should allow one default datasource per organisation [#12229](https://github.com/grafana/grafana/issues/12229) +* **Heatmap**: Fix broken tooltip and crosshair on Firefox [#12486](https://github.com/grafana/grafana/issues/12486) -# 5.2.2 (unreleased) +### Breaking changes + +* Postgres datasource no longer automatically adds time column alias when using the $__timeGroup alias. However, there's code in place which should make this change backward compatible and shouldn't create any issues. + +### New experimental features + +These are new features that's still being worked on and are in an experimental phase. We incourage users to try these out and provide any feedback in related issue. + +* **Dashboard**: Auto fit dashboard panels to optimize space used for current TV / Monitor [#12768](https://github.com/grafana/grafana/issues/12768) + +### Tech + +* **Frontend**: Convert all Frontend Karma tests to Jest tests [#12224](https://github.com/grafana/grafana/issues/12224) + +# 5.2.2 (2018-07-25) ### Minor * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379) * **Dashboard**: Dashboard links not updated when changing variables [#12506](https://github.com/grafana/grafana/issues/12506) +* **Postgres/MySQL/MSSQL**: Fix connection leak [#12636](https://github.com/grafana/grafana/issues/12636) [#9827](https://github.com/grafana/grafana/issues/9827) +* **Plugins**: Fix loading of external plugins [#12551](https://github.com/grafana/grafana/issues/12551) +* **Dashboard**: Remove unwanted scrollbars in embedded panels [#12589](https://github.com/grafana/grafana/issues/12589) +* **Prometheus**: Prevent error using $__interval_ms in query [#12533](https://github.com/grafana/grafana/pull/12533), thx [@mtanda](https://github.com/mtanda) # 5.2.1 (2018-06-29) diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..f7e45893c38 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,82 @@ +# Golang build container +FROM golang:1.10 + +WORKDIR $GOPATH/src/github.com/grafana/grafana + +COPY Gopkg.toml Gopkg.lock ./ +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 + +COPY pkg pkg +COPY build.go build.go +COPY package.json package.json + +RUN go run build.go build + +# Node build container +FROM node:8 + +WORKDIR /usr/src/app/ + +COPY package.json yarn.lock ./ +RUN yarn install --pure-lockfile --no-progress + +COPY Gruntfile.js tsconfig.json tslint.json ./ +COPY public public +COPY scripts scripts +COPY emails emails + +ENV NODE_ENV production +RUN ./node_modules/.bin/grunt build + +# Final container +FROM debian:stretch-slim + +ARG GF_UID="472" +ARG GF_GID="472" + +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" \ + GF_PATHS_DATA="/var/lib/grafana" \ + GF_PATHS_HOME="/usr/share/grafana" \ + GF_PATHS_LOGS="/var/log/grafana" \ + GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ + GF_PATHS_PROVISIONING="/etc/grafana/provisioning" + +WORKDIR $GF_PATHS_HOME + +RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +COPY conf ./conf + +RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ + groupadd -r -g $GF_GID grafana && \ + useradd -r -u $GF_UID -g grafana grafana && \ + mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ + "$GF_PATHS_PROVISIONING/dashboards" \ + "$GF_PATHS_LOGS" \ + "$GF_PATHS_PLUGINS" \ + "$GF_PATHS_DATA" && \ + cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \ + cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \ + chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \ + chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" + +COPY --from=0 /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-server /go/src/github.com/grafana/grafana/bin/linux-amd64/grafana-cli ./bin/ +COPY --from=1 /usr/src/app/public ./public +COPY --from=1 /usr/src/app/tools ./tools +COPY tools/phantomjs/render.js ./tools/phantomjs/render.js + +EXPOSE 3000 + +COPY ./packaging/docker/run.sh /run.sh + +USER grafana +ENTRYPOINT [ "/run.sh" ] diff --git a/Gopkg.lock b/Gopkg.lock index 5acaf2a542c..6f08e208ecd 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -32,6 +32,7 @@ "aws/credentials/ec2rolecreds", "aws/credentials/endpointcreds", "aws/credentials/stscreds", + "aws/csm", "aws/defaults", "aws/ec2metadata", "aws/endpoints", @@ -43,6 +44,8 @@ "internal/shareddefaults", "private/protocol", "private/protocol/ec2query", + "private/protocol/eventstream", + "private/protocol/eventstream/eventstreamapi", "private/protocol/query", "private/protocol/query/queryutil", "private/protocol/rest", @@ -54,8 +57,8 @@ "service/s3", "service/sts" ] - revision = "c7cd1ebe87257cde9b65112fc876b0339ea0ac30" - version = "v1.13.49" + revision = "fde4ded7becdeae4d26bf1212916aabba79349b4" + version = "v1.14.12" [[projects]] branch = "master" @@ -424,6 +427,12 @@ revision = "1744e2970ca51c86172c8190fadad617561ed6e7" version = "v1.0.0" +[[projects]] + branch = "master" + name = "github.com/shurcooL/sanitized_anchor_name" + packages = ["."] + revision = "86672fcb3f950f35f2e675df2240550f2a50762f" + [[projects]] name = "github.com/smartystreets/assertions" packages = [ @@ -670,6 +679,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7" + inputs-digest = "cb8e7fd81f23ec987fc4d5dd9d31ae0f1164bc2f30cbea2fe86e0d97dd945beb" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index 1768059f0b8..6c91ec37221 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -36,7 +36,7 @@ ignored = [ [[constraint]] name = "github.com/aws/aws-sdk-go" - version = "1.12.65" + version = "1.13.56" [[constraint]] branch = "master" diff --git a/Gruntfile.js b/Gruntfile.js index 23276e8a122..8a71fb44148 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,4 +1,3 @@ -/* jshint node:true */ 'use strict'; module.exports = function (grunt) { var os = require('os'); diff --git a/Makefile b/Makefile index c1d755d247d..c6915409ed7 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,15 @@ build-js: build: build-go build-js +build-docker-dev: + @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already." + go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest + cp dist/grafana-latest.linux-x64.tar.gz packaging/docker + cd packaging/docker && docker build --tag grafana/grafana:dev . + +build-docker-full: + docker build --tag grafana/grafana:dev . + test-go: go test -v ./pkg/... @@ -36,4 +45,4 @@ run: ./bin/grafana-server protoc: - protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/. \ No newline at end of file + protoc -I pkg/tsdb/models pkg/tsdb/models/*.proto --go_out=plugins=grpc:pkg/tsdb/models/. diff --git a/NOTICE.md b/NOTICE.md index ca148971b62..899b2a3c3f9 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,5 +1,5 @@ -Copyright 2014-2017 Grafana Labs +Copyright 2014-2018 Grafana Labs This software is based on Kibana: Copyright 2012-2013 Elasticsearch BV diff --git a/README.md b/README.md index 322523d703b..74fb10c8066 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ To build the assets, rebuild on file change, and serve them by Grafana's webserv ```bash npm install -g yarn yarn install --pure-lockfile -npm run watch +yarn watch ``` Build the assets, rebuild on file change with Hot Module Replacement (HMR), and serve them by webpack-dev-server (http://localhost:3333): @@ -54,14 +54,9 @@ env GRAFANA_THEME=light yarn start ``` Note: HMR for Angular is not supported. If you edit files in the Angular part of the app, the whole page will reload. -Run tests +Run tests ```bash -npm run jest -``` - -Run karma tests -```bash -npm run karma +yarn jest ``` ### Recompile backend on source change @@ -74,6 +69,15 @@ bra run Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). +### Building a docker image (on linux/amd64) + +This builds a docker image from your local sources: + +1. Build the frontend `go run build.go build-frontend` +2. Build the docker image `make build-docker-dev` + +The resulting image will be tagged as `grafana/grafana:dev` + ### Dev config Create a custom.ini in the conf directory to override default configuration options. @@ -89,30 +93,38 @@ In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = #### Frontend Execute all frontend tests ```bash -npm run test +yarn test ``` -Writing & watching frontend tests (we have two test runners) +Writing & watching frontend tests -- jest for all new tests that do not require browser context (React+more) - - Start watcher: `npm run jest` - - Jest will run all test files that end with the name ".jest.ts" -- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). - - Start watcher: `npm run karma` - - Karma+Mocha runs all files that end with the name "_specs.ts". +- Start watcher: `yarn jest` +- Jest will run all test files that end with the name ".test.ts" #### Backend ```bash # Run Golang tests using sqlite3 as database (default) -go test ./pkg/... +go test ./pkg/... # Run Golang tests using mysql as database - convenient to use /docker/blocks/mysql_tests -GRAFANA_TEST_DB=mysql go test ./pkg/... +GRAFANA_TEST_DB=mysql go test ./pkg/... # Run Golang tests using postgres as database - convenient to use /docker/blocks/postgres_tests -GRAFANA_TEST_DB=postgres go test ./pkg/... +GRAFANA_TEST_DB=postgres go test ./pkg/... ``` +## Building custom docker image + +You can build a custom image using Docker, which doesn't require installing any dependencies besides docker itself. +```bash +git clone https://github.com/grafana/grafana +cd grafana +docker build -t grafana:dev . +docker run -d --name=grafana -p 3000:3000 grafana:dev +``` + +Open grafana in your browser (default: `http://localhost:3000`) and login with admin user (default: `user/pass = admin/admin`). + ## Contribute If you have any idea for an improvement or found a bug, do not hesitate to open an issue. diff --git a/ROADMAP.md b/ROADMAP.md index 6f8111fd2d4..891bc9f790b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,9 +1,10 @@ -# Roadmap (2018-06-26) +# Roadmap (2018-08-07) This roadmap is a tentative plan for the core development team. Things change constantly as PRs come in and priorities change. But it will give you an idea of our current vision and plan. ### Short term (1-2 months) + - PRs & Bugs - Multi-Stat panel - Metrics & Log Explore UI @@ -11,17 +12,16 @@ But it will give you an idea of our current vision and plan. - React Panels - Change visualization (panel type) on the fly. - Templating Query Editor UI Plugin hook + - Backend plugins ### Long term (4 - 8 months) - -- Alerting improvements (silence, per series tracking, etc) -- Progress on React migration + - Alerting improvements (silence, per series tracking, etc) + - Progress on React migration ### In a distant future far far away - -- Meta queries -- Integrated light weight TSDB -- Web socket & live data sources + - Meta queries + - Integrated light weight TSDB + - Web socket & live data sources ### Outside contributions We know this is being worked on right now by contributors (and we hope to merge it when it's ready). diff --git a/build.go b/build.go index 77cbde50c41..561dd70df0e 100644 --- a/build.go +++ b/build.go @@ -64,6 +64,10 @@ func main() { readVersionFromPackageJson() + if pkgArch == "" { + pkgArch = goarch + } + log.Printf("Version: %s, Linux Version: %s, Package Iteration: %s\n", version, linuxPackageVersion, linuxPackageIteration) if flag.NArg() == 0 { @@ -105,10 +109,17 @@ func main() { case "package": grunt(gruntBuildArg("build")...) - packageGrafana() + grunt(gruntBuildArg("package")...) + if goos == "linux" { + createLinuxPackages() + } case "package-only": - packageGrafana() + grunt(gruntBuildArg("package")...) + if goos == "linux" { + createLinuxPackages() + } + case "pkg-rpm": grunt(gruntBuildArg("release")...) @@ -133,22 +144,6 @@ func main() { } } -func packageGrafana() { - platformArg := fmt.Sprintf("--platform=%v", goos) - previousPkgArch := pkgArch - if pkgArch == "" { - pkgArch = goarch - } - postProcessArgs := gruntBuildArg("package") - postProcessArgs = append(postProcessArgs, platformArg) - grunt(postProcessArgs...) - pkgArch = previousPkgArch - - if goos == "linux" { - createLinuxPackages() - } -} - func makeLatestDistCopies() { files, err := ioutil.ReadDir("dist") if err != nil { @@ -330,6 +325,7 @@ func createPackage(options linuxPackageOptions) { name := "grafana" if enterprise { name += "-enterprise" + args = append(args, "--replaces", "grafana") } args = append(args, "--name", name) @@ -403,6 +399,8 @@ func gruntBuildArg(task string) []string { if phjsToRelease != "" { args = append(args, fmt.Sprintf("--phjsToRelease=%v", phjsToRelease)) } + args = append(args, fmt.Sprintf("--platform=%v", goos)) + return args } diff --git a/conf/defaults.ini b/conf/defaults.ini index 5faba3ea7bd..90fc144c6e0 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -213,6 +213,9 @@ allow_org_create = false # Set to true to automatically assign new users to the default organization (id 1) auto_assign_org = true +# Set this value to automatically add new users to the provided organization (if auto_assign_org above is set to true) +auto_assign_org_id = 1 + # Default role new users will be automatically assigned (if auto_assign_org above is set to true) auto_assign_org_role = Viewer @@ -267,6 +270,18 @@ api_url = https://api.github.com/user team_ids = allowed_organizations = +#################################### GitLab Auth ######################### +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = some_id +client_secret = some_secret +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = + #################################### Google Auth ######################### [auth.google] enabled = false @@ -311,6 +326,10 @@ token_url = api_url = team_ids = allowed_organizations = +tls_skip_verify_insecure = false +tls_client_cert = +tls_client_key = +tls_client_ca = #################################### Basic Auth ########################## [auth.basic] diff --git a/conf/ldap.toml b/conf/ldap.toml index 166d85eabb1..9a7088ed823 100644 --- a/conf/ldap.toml +++ b/conf/ldap.toml @@ -15,6 +15,9 @@ start_tls = false ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults # root_ca_cert = "/path/to/certificate.crt" +# Authentication against LDAP servers requiring client certificates +# client_cert = "/path/to/client.crt" +# client_key = "/path/to/client.key" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" @@ -72,6 +75,8 @@ email = "email" [[servers.group_mappings]] group_dn = "cn=admins,dc=grafana,dc=org" org_role = "Admin" +# To make user an instance admin (Grafana Admin) uncomment line below +# grafana_admin = true # The Grafana organization database id, optional, if left out the default org (id 1) will be used # org_id = 1 diff --git a/conf/sample.ini b/conf/sample.ini index 87544a5ac39..4291071e026 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -272,6 +272,10 @@ log_queries = ;api_url = https://foo.bar/user ;team_ids = ;allowed_organizations = +;tls_skip_verify_insecure = false +;tls_client_cert = +;tls_client_key = +;tls_client_ca = #################################### Grafana.com Auth #################### [auth.grafana_com] diff --git a/devenv/README.md b/devenv/README.md index 4ec6f672f25..9abf3596776 100644 --- a/devenv/README.md +++ b/devenv/README.md @@ -1,11 +1,16 @@ This folder contains useful scripts and configuration for... -* Configuring datasources in Grafana -* Provision example dashboards in Grafana -* Run preconfiured datasources as docker containers - -want to know more? run setup! +* Configuring dev datasources in Grafana +* Configuring dev & test scenarios dashboards. ```bash ./setup.sh ``` + +After restarting grafana server there should now be a number of datasources named `gdev-` provisioned as well as a dashboard folder named `gdev dashboards`. This folder contains dashboard & panel features tests dashboards. + +# Dev dashboards + +Please update these dashboards or make new ones as new panels & dashboards features are developed or new bugs are found. The dashboards are located in the `devenv/dev-dashboards` folder. + + diff --git a/devenv/bulk-dashboards/bulk-dashboards.yaml b/devenv/bulk-dashboards/bulk-dashboards.yaml index e0ba8a88e68..65557901f42 100644 --- a/devenv/bulk-dashboards/bulk-dashboards.yaml +++ b/devenv/bulk-dashboards/bulk-dashboards.yaml @@ -5,5 +5,5 @@ providers: folder: 'Bulk dashboards' type: file options: - path: devenv/dashboards/bulk-testing + path: devenv/bulk-dashboards diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index e93c0217f27..a4e9bf05641 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -14,6 +14,9 @@ datasources: isDefault: true url: http://localhost:9090 + - name: gdev-testdata + type: testdata + - name: gdev-influxdb type: influxdb access: proxy @@ -48,19 +51,46 @@ datasources: user: grafana password: password + - name: gdev-mysql-ds-tests + type: mysql + url: localhost:3306 + database: grafana_ds_tests + user: grafana + password: password + - name: gdev-mssql type: mssql url: localhost:1433 database: grafana user: grafana - password: "Password!" + secureJsonData: + password: Password! + + - name: gdev-mssql-ds-tests + type: mssql + url: localhost:1433 + database: grafanatest + user: grafana + secureJsonData: + password: Password! - name: gdev-postgres type: postgres url: localhost:5432 database: grafana user: grafana - password: password + secureJsonData: + password: password + jsonData: + sslmode: "disable" + + - name: gdev-postgres-ds-tests + type: postgres + url: localhost:5432 + database: grafanadstest + user: grafanatest + secureJsonData: + password: grafanatest jsonData: sslmode: "disable" @@ -71,3 +101,4 @@ datasources: authType: credentials defaultRegion: eu-west-2 + diff --git a/devenv/dev-dashboards/dashboard_with_rows.json b/devenv/dev-dashboards/dashboard_with_rows.json deleted file mode 100644 index 335c27bc80a..00000000000 --- a/devenv/dev-dashboards/dashboard_with_rows.json +++ /dev/null @@ -1,592 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": "-- Grafana --", - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "type": "dashboard" - } - ] - }, - "editable": true, - "gnetId": null, - "graphTooltip": 0, - "id": 59, - "links": [], - "panels": [ - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 9, - "panels": [], - "title": "Row title", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 0, - "y": 1 - }, - "id": 12, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 12, - "y": 1 - }, - "id": 5, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 5 - }, - "id": 7, - "panels": [], - "title": "Row", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 0, - "y": 6 - }, - "id": 2, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 12, - "y": 6 - }, - "id": 13, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "collapsed": false, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 10 - }, - "id": 11, - "panels": [], - "title": "Row title", - "type": "row" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 0, - "y": 11 - }, - "id": 4, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "Prometheus", - "fill": 1, - "gridPos": { - "h": 4, - "w": 12, - "x": 12, - "y": 11 - }, - "id": 3, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "nullPointMode": "null", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "expr": "go_goroutines", - "format": "time_series", - "intervalFactor": 1, - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Panel Title", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - } - ], - "schemaVersion": 16, - "style": "dark", - "tags": [], - "templating": { - "list": [] - }, - "time": { - "from": "now-30m", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "", - "title": "Dashboard with rows", - "uid": "1DdOzBNmk", - "version": 5 -} diff --git a/docker/blocks/mssql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json similarity index 92% rename from docker/blocks/mssql/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mssql_fakedata.json index ce9aa141a75..e810a686134 100644 --- a/docker/blocks/mssql/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MSSQL", - "label": "MSSQL", - "description": "", - "type": "datasource", - "pluginId": "mssql", - "pluginName": "MSSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mssql", - "name": "MSSQL", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -52,8 +16,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1520976748896, + "iteration": 1532618661457, "links": [], "panels": [ { @@ -63,7 +26,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 9, @@ -149,14 +112,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 18, @@ -234,14 +201,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fill": 2, "gridPos": { "h": 9, @@ -313,11 +284,15 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "columns": [], - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "fontSize": "100%", "gridPos": { "h": 10, @@ -371,13 +346,13 @@ ], "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": ["gdev", "mssql", "fake-data-gen"], "templating": { "list": [ { "allValue": null, "current": {}, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -387,6 +362,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -397,7 +373,7 @@ { "allValue": null, "current": {}, - "datasource": "${DS_MSSQL}", + "datasource": "gdev-mssql", "hide": 0, "includeAll": true, "label": "Hostname", @@ -407,6 +383,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -499,6 +476,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -533,7 +511,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - MSSQL", + "title": "Datasource tests - MSSQL", "uid": "86Js1xRmk", - "version": 11 + "version": 1 } \ No newline at end of file diff --git a/docker/blocks/mysql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json similarity index 70% rename from docker/blocks/mysql_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 53f313315bd..b2d757ae188 100644 --- a/docker/blocks/mysql_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MYSQL_TEST", - "label": "MySQL TEST", - "description": "", - "type": "datasource", - "pluginId": "mysql", - "pluginName": "MySQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mysql", - "name": "MySQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,31 +11,31 @@ "type": "dashboard" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", "limit": 100, "name": "Deploys", - "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", "limit": 100, "name": "Tickets", - "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,16 @@ } ] }, + "description": "Run the mssql unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320712115, + "iteration": 1534507501976, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -142,7 +106,7 @@ { "alias": "", "format": "table", - "rawSql": "SELECT * from mysql_types", + "rawSql": "SELECT * from mssql_types", "refId": "A" } ], @@ -152,7 +116,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -173,7 +137,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time_sec", + "pattern": "time", "type": "date" }, { @@ -195,18 +159,18 @@ { "alias": "", "format": "table", - "rawSql": "SELECT cast(null as unsigned integer) as time_sec", + "rawSql": "SELECT cast(null as bigint) as time", "refId": "A", "target": "" } ], - "title": "cast(null as unsigned integer) as time", + "title": "cast(null as bigint) as time", "transform": "table", "type": "table" }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -227,7 +191,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time_sec", + "pattern": "time", "type": "date" }, { @@ -249,7 +213,7 @@ { "alias": "", "format": "table", - "rawSql": "SELECT cast(null as datetime) as time_sec", + "rawSql": "SELECT cast(null as datetime) as time", "refId": "A", "target": "" } @@ -260,7 +224,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -281,7 +245,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time_sec", + "pattern": "time", "type": "date" }, { @@ -303,18 +267,18 @@ { "alias": "", "format": "table", - "rawSql": "SELECT cast(NOW() as datetime) as time_sec", + "rawSql": "SELECT GETDATE() as time", "refId": "A", "target": "" } ], - "title": "cast()NOW() as datetime) as time", + "title": "GETDATE() as time", "transform": "table", "type": "table" }, { "columns": [], - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -357,12 +321,12 @@ { "alias": "", "format": "table", - "rawSql": "SELECT NOW() as time", + "rawSql": "SELECT GETUTCDATE() as time", "refId": "A", "target": "" } ], - "title": "NOW() as time", + "title": "GETUTCDATE() as time", "transform": "table", "type": "table" }, @@ -371,11 +335,11 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -405,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -454,12 +418,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -488,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -537,12 +501,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -571,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", "refId": "A" } ], @@ -615,18 +579,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous) and null as zero", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -654,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -703,13 +750,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -737,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -786,13 +833,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -820,7 +867,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", "refId": "A" } ], @@ -864,18 +911,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -907,14 +1037,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value one') as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n CONCAT(measurement, ' - value two') as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1,2\nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -962,13 +1086,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1010,8 +1134,14 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1", + "refId": "B" } ], "thresholds": [], @@ -1059,13 +1189,203 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 + }, + "id": 38, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__unixEpochGroup(timeInt32, '$summarize'), \n measurement \nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 39, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__unixEpochGroup(timeInt32, '$summarize')\nORDER BY 1", + "refId": "A" + }, + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1", + "refId": "B" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 }, "id": 4, "legend": { @@ -1097,14 +1417,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1152,13 +1466,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 35 }, "id": 28, "legend": { @@ -1188,7 +1502,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", "refId": "A" } ], @@ -1237,13 +1551,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 43 }, "id": 19, "legend": { @@ -1275,14 +1589,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1330,13 +1638,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 43 }, "id": 18, "legend": { @@ -1366,7 +1674,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1415,13 +1723,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 51 }, "id": 17, "legend": { @@ -1453,14 +1761,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__time(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1508,13 +1810,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 51 }, "id": 20, "legend": { @@ -1544,7 +1846,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1588,18 +1890,192 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 59 + }, + "id": 29, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stored procedure support using epoch", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mssql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 59 + }, + "id": 30, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stored procedure support using datetime", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 67 }, "id": 14, "legend": { @@ -1629,14 +2105,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1687,13 +2157,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 67 }, "id": 15, "legend": { @@ -1723,7 +2193,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1774,13 +2244,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 75 }, "id": 25, "legend": { @@ -1810,14 +2280,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1868,13 +2332,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 75 }, "id": 22, "legend": { @@ -1904,7 +2368,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -1955,13 +2419,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 83 }, "id": 21, "legend": { @@ -1991,14 +2455,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2049,13 +2507,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 83 }, "id": 26, "legend": { @@ -2085,7 +2543,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -2136,13 +2594,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 91 }, "id": 23, "legend": { @@ -2172,14 +2630,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value one') as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), CONCAT(measurement, ' - value two') as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2230,13 +2682,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL_TEST}", + "datasource": "gdev-mssql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 91 }, "id": 24, "legend": { @@ -2266,7 +2718,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", "refId": "A" } ], @@ -2316,22 +2768,47 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": [ + "gdev", + "mssql" + ], "templating": { "list": [ { - "allValue": "", - "current": {}, - "datasource": "${DS_MYSQL_TEST}", + "allValue": "'ALL'", + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mssql-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", - "multi": true, + "multi": false, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], @@ -2384,6 +2861,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2418,7 +2896,7 @@ ] }, "timezone": "", - "title": "MySQL Data Source Test", - "uid": "Hmf8FDkmz", - "version": 12 + "title": "Datasource tests - MSSQL (unit test)", + "uid": "GlAqcPgmz", + "version": 2 } \ No newline at end of file diff --git a/docker/blocks/mysql/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json similarity index 92% rename from docker/blocks/mysql/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mysql_fakedata.json index dba7847cc72..ebeb452fc4c 100644 --- a/docker/blocks/mysql/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MYSQL", - "label": "MySQL", - "description": "", - "type": "datasource", - "pluginId": "mysql", - "pluginName": "MySQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mysql", - "name": "MySQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -52,8 +16,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523372133566, + "iteration": 1532620738041, "links": [], "panels": [ { @@ -63,7 +26,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 9, @@ -161,7 +124,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 18, @@ -251,7 +214,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fill": 2, "gridPos": { "h": 9, @@ -332,7 +295,7 @@ }, { "columns": [], - "datasource": "${DS_MYSQL}", + "datasource": "gdev-mysql", "fontSize": "100%", "gridPos": { "h": 9, @@ -390,6 +353,7 @@ "schemaVersion": 16, "style": "dark", "tags": [ + "gdev", "fake-data-gen", "mysql" ], @@ -397,8 +361,11 @@ "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_MYSQL}", + "current": { + "text": "America", + "value": "America" + }, + "datasource": "gdev-mysql", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -408,6 +375,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -417,8 +385,11 @@ }, { "allValue": null, - "current": {}, - "datasource": "${DS_MYSQL}", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mysql", "hide": 0, "includeAll": true, "label": "Hostname", @@ -428,6 +399,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -520,6 +492,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -554,7 +527,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - MySQL", + "title": "Datasource tests - MySQL", "uid": "DGsCac3kz", "version": 8 } \ No newline at end of file diff --git a/docker/blocks/mssql_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json similarity index 81% rename from docker/blocks/mssql_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_mysql_unittest.json index 80994254093..0255f3c0c91 100644 --- a/docker/blocks/mssql_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_MSSQL_TEST", - "label": "MSSQL Test", - "description": "", - "type": "datasource", - "pluginId": "mssql", - "pluginName": "Microsoft SQL Server" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "mssql", - "name": "Microsoft SQL Server", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,31 +11,31 @@ "type": "dashboard" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", "limit": 100, "name": "Deploys", - "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n time_sec,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='deploy'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", "limit": 100, "name": "Tickets", - "rawQuery": "SELECT\n $__time(time_sec),\n description as [text],\n tags\n FROM [event]\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", + "rawQuery": "SELECT\n time_sec as time,\n description as text,\n tags\n FROM event\n WHERE $__unixEpochFilter(time_sec) AND tags='ticket'\n ORDER BY 1 ASC\n ", "showIn": 0, "tags": [], "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,16 @@ } ] }, + "description": "Run the mysql unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320861623, + "iteration": 1534508678095, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -142,7 +106,7 @@ { "alias": "", "format": "table", - "rawSql": "SELECT * from mssql_types", + "rawSql": "SELECT * from mysql_types", "refId": "A" } ], @@ -152,7 +116,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -173,7 +137,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time", + "pattern": "time_sec", "type": "date" }, { @@ -195,18 +159,18 @@ { "alias": "", "format": "table", - "rawSql": "SELECT cast(null as bigint) as time", + "rawSql": "SELECT cast(null as unsigned integer) as time_sec", "refId": "A", "target": "" } ], - "title": "cast(null as bigint) as time", + "title": "cast(null as unsigned integer) as time", "transform": "table", "type": "table" }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -227,7 +191,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time", + "pattern": "time_sec", "type": "date" }, { @@ -249,7 +213,7 @@ { "alias": "", "format": "table", - "rawSql": "SELECT cast(null as datetime) as time", + "rawSql": "SELECT cast(null as datetime) as time_sec", "refId": "A", "target": "" } @@ -260,7 +224,7 @@ }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -281,7 +245,7 @@ { "alias": "Time", "dateFormat": "YYYY-MM-DD HH:mm:ss", - "pattern": "time", + "pattern": "time_sec", "type": "date" }, { @@ -303,18 +267,18 @@ { "alias": "", "format": "table", - "rawSql": "SELECT GETDATE() as time", + "rawSql": "SELECT cast(NOW() as datetime) as time_sec", "refId": "A", "target": "" } ], - "title": "GETDATE() as time", + "title": "cast()NOW() as datetime) as time", "transform": "table", "type": "table" }, { "columns": [], - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -357,12 +321,12 @@ { "alias": "", "format": "table", - "rawSql": "SELECT GETUTCDATE() as time", + "rawSql": "SELECT NOW() as time", "refId": "A", "target": "" } ], - "title": "GETUTCDATE() as time", + "title": "NOW() as time", "transform": "table", "type": "table" }, @@ -371,11 +335,11 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -405,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -454,12 +418,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -488,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -537,12 +501,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -571,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0) AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '5m') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -615,18 +579,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -654,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize') AS time, avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -703,13 +750,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -737,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -786,13 +833,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -820,7 +867,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0) AS time, sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY $__timeGroup(time, '$summarize') ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -864,18 +911,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -907,14 +1037,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value one' as metric, \n avg(valueOne) as valueOne\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement as metric, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement IN($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n measurement + ' - value two' as metric, \n avg(valueTwo) as valueTwo \nFROM\n metric_values\nWHERE\n $__timeFilter(time) AND\n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize'), \n measurement \nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -962,13 +1086,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1010,14 +1134,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize') as time, \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nGROUP BY \n $__timeGroup(time, '$summarize')\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(valueOne) as valueOne, \n avg(valueTwo) as valueTwo \nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n measurement IN($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n time,\n avg(valueOne) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueOne,\n avg(valueTwo) OVER (ORDER BY time ROWS BETWEEN 6 PRECEDING AND 6 FOLLOWING) as MovingAverageValueTwo\nFROM\n metric_values \nWHERE \n $__timeFilter(time) AND \n ($metric = 'ALL' OR measurement = $metric)\nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1065,13 +1183,197 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 + }, + "id": 38, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n measurement, \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 39, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "MovingAverageValueOne", + "dashes": true, + "lines": false + }, + { + "alias": "MovingAverageValueTwo", + "dashes": true, + "lines": false, + "yaxis": 1 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(timeInt32, '$summarize'), \n avg(valueOne) as valueOne,\n avg(valueTwo) as valueTwo\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(timeInt32) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using unixEpochGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-mysql-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 }, "id": 4, "legend": { @@ -1103,14 +1405,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1158,13 +1454,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 35 }, "id": 28, "legend": { @@ -1194,7 +1490,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values ORDER BY 1", + "rawSql": "SELECT $__time(time), valueOne, valueTwo FROM metric_values ORDER BY 1", "refId": "A" } ], @@ -1243,13 +1539,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 43 }, "id": 19, "legend": { @@ -1281,14 +1577,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1336,13 +1626,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 43 }, "id": 18, "legend": { @@ -1372,7 +1662,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -1421,13 +1711,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 51 }, "id": 17, "legend": { @@ -1459,14 +1749,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values WHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1514,13 +1798,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 51 }, "id": 20, "legend": { @@ -1550,7 +1834,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -1594,192 +1878,18 @@ "alignLevel": null } }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_MSSQL_TEST}", - "fill": 2, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 57 - }, - "id": 29, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "hideEmpty": false, - "hideZero": false, - "max": true, - "min": true, - "rightSide": true, - "show": true, - "total": true, - "values": true - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 3, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "rawSql": "DECLARE\n @from int = $__unixEpochFrom(), \n @to int = $__unixEpochTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_epoch @from, @to, @interval, @metric", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Stored procedure support using epoch", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": "${DS_MSSQL_TEST}", - "fill": 2, - "gridPos": { - "h": 8, - "w": 12, - "x": 12, - "y": 57 - }, - "id": 30, - "legend": { - "alignAsTable": true, - "avg": true, - "current": true, - "hideEmpty": false, - "hideZero": false, - "max": true, - "min": true, - "rightSide": true, - "show": true, - "total": true, - "values": true - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "null", - "percentage": false, - "pointradius": 3, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "spaceLength": 10, - "stack": false, - "steppedLine": false, - "targets": [ - { - "alias": "", - "format": "time_series", - "rawSql": "DECLARE\n @from datetime = $__timeFrom(), \n @to datetime = $__timeTo(), \n @interval nvarchar(50) = '$summarize', \n @metric nvarchar(200) = $metric\n \nEXEC dbo.sp_test_datetime @from, @to, @interval, @metric", - "refId": "A" - } - ], - "thresholds": [], - "timeFrom": null, - "timeShift": null, - "title": "Stored procedure support using datetime", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "buckets": null, - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ], - "yaxis": { - "align": false, - "alignLevel": null - } - }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 14, "legend": { @@ -1809,14 +1919,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1867,13 +1971,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 15, "legend": { @@ -1903,7 +2007,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -1954,13 +2058,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 25, "legend": { @@ -1990,14 +2094,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2048,13 +2146,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 22, "legend": { @@ -2084,7 +2182,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -2135,13 +2233,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 21, "legend": { @@ -2171,14 +2269,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2229,13 +2321,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 26, "legend": { @@ -2265,7 +2357,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -2316,13 +2408,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 89 + "y": 83 }, "id": 23, "legend": { @@ -2352,14 +2444,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value one' as metric, valueOne FROM metric_values\nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values WHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement + ' - value two' as metric, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2410,13 +2496,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_MSSQL_TEST}", + "datasource": "gdev-mysql-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 89 + "y": 83 }, "id": 24, "legend": { @@ -2446,7 +2532,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values \nWHERE $__timeFilter(time) AND ($metric = 'ALL' OR measurement = $metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), valueOne, valueTwo FROM metric_values\nWHERE $__timeFilter(time) AND measurement IN($metric) ORDER BY 1", "refId": "A" } ], @@ -2496,22 +2582,45 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": [ + "gdev", + "mysql" + ], "templating": { "list": [ { - "allValue": "'ALL'", - "current": {}, - "datasource": "${DS_MSSQL_TEST}", + "allValue": "", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-mysql-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", - "multi": false, + "multi": true, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 0, "tagValuesQuery": "", "tags": [], @@ -2564,6 +2673,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2598,7 +2708,7 @@ ] }, "timezone": "", - "title": "Microsoft SQL Server Data Source Test", - "uid": "GlAqcPgmz", - "version": 58 + "title": "Datasource tests - MySQL (unittest)", + "uid": "Hmf8FDkmz", + "version": 2 } \ No newline at end of file diff --git a/docker/blocks/postgres/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json similarity index 91% rename from docker/blocks/postgres/dashboard.json rename to devenv/dev-dashboards/datasource_tests_postgres_fakedata.json index 77b0ceac624..508cae86bc3 100644 --- a/docker/blocks/postgres/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_fakedata.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_POSTGRESQL", - "label": "PostgreSQL", - "description": "", - "type": "datasource", - "pluginId": "postgres", - "pluginName": "PostgreSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "" - }, - { - "type": "datasource", - "id": "postgres", - "name": "PostgreSQL", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "" - } - ], "annotations": { "list": [ { @@ -52,8 +16,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1518601837383, + "iteration": 1532620601931, "links": [], "panels": [ { @@ -63,7 +26,7 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 9, @@ -150,14 +113,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 18, @@ -236,14 +203,18 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fill": 2, "gridPos": { "h": 9, @@ -316,11 +287,15 @@ "min": null, "show": true } - ] + ], + "yaxis": { + "align": false, + "alignLevel": null + } }, { "columns": [], - "datasource": "${DS_POSTGRESQL}", + "datasource": "gdev-postgres", "fontSize": "100%", "gridPos": { "h": 9, @@ -377,6 +352,7 @@ "schemaVersion": 16, "style": "dark", "tags": [ + "gdev", "fake-data-gen", "postgres" ], @@ -384,8 +360,11 @@ "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRESQL}", + "current": { + "text": "America", + "value": "America" + }, + "datasource": "gdev-postgres", "hide": 0, "includeAll": false, "label": "Datacenter", @@ -395,6 +374,7 @@ "query": "SELECT DISTINCT datacenter FROM grafana_metric", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -404,8 +384,11 @@ }, { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRESQL}", + "current": { + "text": "All", + "value": "$__all" + }, + "datasource": "gdev-postgres", "hide": 0, "includeAll": true, "label": "Hostname", @@ -415,6 +398,7 @@ "query": "SELECT DISTINCT hostname FROM grafana_metric WHERE datacenter='$datacenter'", "refresh": 1, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -507,6 +491,7 @@ ], "query": "1s,10s,30s,1m,5m,10m,30m,1h,6h,12h,1d,7d,14d,30d", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -541,7 +526,7 @@ ] }, "timezone": "", - "title": "Grafana Fake Data Gen - PostgreSQL", + "title": "Datasource tests - Postgres", "uid": "JYola5qzz", - "version": 1 + "version": 4 } \ No newline at end of file diff --git a/docker/blocks/postgres_tests/dashboard.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json similarity index 78% rename from docker/blocks/postgres_tests/dashboard.json rename to devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 9efbe90bdfe..3c56868e9ff 100644 --- a/docker/blocks/postgres_tests/dashboard.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -1,40 +1,4 @@ { - "__inputs": [ - { - "name": "DS_POSTGRES_TEST", - "label": "Postgres TEST", - "description": "", - "type": "datasource", - "pluginId": "postgres", - "pluginName": "PostgreSQL" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "graph", - "name": "Graph", - "version": "5.0.0" - }, - { - "type": "datasource", - "id": "postgres", - "name": "PostgreSQL", - "version": "5.0.0" - }, - { - "type": "panel", - "id": "table", - "name": "Table", - "version": "5.0.0" - } - ], "annotations": { "list": [ { @@ -47,7 +11,7 @@ "type": "dashboard" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#6ed0e0", @@ -59,7 +23,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "rgba(255, 96, 96, 1)", @@ -71,7 +35,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#7eb26d", @@ -83,7 +47,7 @@ "type": "tags" }, { - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "enable": false, "hide": false, "iconColor": "#1f78c1", @@ -96,16 +60,16 @@ } ] }, + "description": "Run the postgres unit tests to generate the data backing this dashboard", "editable": true, "gnetId": null, "graphTooltip": 0, - "id": null, - "iteration": 1523320929325, + "iteration": 1534507993194, "links": [], "panels": [ { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 4, @@ -152,7 +116,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -206,7 +170,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -260,7 +224,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -314,7 +278,7 @@ }, { "columns": [], - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fontSize": "100%", "gridPos": { "h": 3, @@ -371,11 +335,11 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -405,7 +369,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -454,12 +418,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -488,7 +452,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', NULL), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -537,12 +501,12 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -571,7 +535,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', 10.0), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -615,18 +579,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 36, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": true, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '5m', previous), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "timeGroup macro 5m with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, - "y": 16 + "y": 13 }, "id": 16, "legend": { @@ -654,7 +701,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize'), avg(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -703,13 +750,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, - "y": 16 + "h": 6, + "w": 6, + "x": 6, + "y": 13 }, "id": 12, "legend": { @@ -737,7 +784,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', NULL), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -786,13 +833,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, - "y": 16 + "h": 6, + "w": 6, + "x": 12, + "y": 13 }, "id": 13, "legend": { @@ -820,7 +867,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeGroup(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', 100.0), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", "refId": "A" } ], @@ -864,18 +911,101 @@ "alignLevel": null } }, + { + "aliasColors": {}, + "bars": true, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 13 + }, + "id": 37, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": false, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": true, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT $__timeGroupAlias(time, '$summarize', previous), sum(value) as value FROM metric WHERE $__timeFilter(time) GROUP BY 1 ORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Metrics - timeGroup macro $summarize with fill(previous)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, { "aliasColors": {}, "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -907,14 +1037,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value one' as metric, \n avg(\"valueOne\") as \"valueOne\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n measurement || ' - value two' as metric, \n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -962,13 +1086,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -998,7 +1122,7 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT \n $__timeGroup(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "rawSql": "SELECT \n $__timeGroupAlias(time, '$summarize'), \n avg(\"valueOne\") as \"valueOne\", \n avg(\"valueTwo\") as \"valueTwo\" \nFROM\n metric_values \nWHERE\n $__timeFilter(time) AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", "refId": "A" } ], @@ -1047,13 +1171,185 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 + }, + "id": 38, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "hideEmpty": false, + "hideZero": false, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n measurement, \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(\"timeInt32\") AND\n measurement in($metric)\nGROUP BY 1, 2\nORDER BY 1, 2", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series with metric column using unixEpochGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 39, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 3, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "alias": "", + "format": "time_series", + "rawSql": "SELECT \n $__unixEpochGroupAlias(\"timeInt32\", '$summarize'), \n avg(\"valueOne\") as \"valueOne\",\n avg(\"valueTwo\") as \"valueTwo\"\nFROM\n metric_values \nWHERE\n $__unixEpochFilter(\"timeInt32\") AND\n measurement in($metric)\nGROUP BY 1\nORDER BY 1", + "refId": "A" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Multiple series without metric column using timeGroup macro ($summarize)", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-postgres-ds-tests", + "fill": 2, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 }, "id": 4, "legend": { @@ -1085,14 +1381,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1140,13 +1430,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 35 }, "id": 28, "legend": { @@ -1225,13 +1515,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 43 }, "id": 19, "legend": { @@ -1263,14 +1553,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1318,13 +1602,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 43 }, "id": 18, "legend": { @@ -1403,13 +1687,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 51 }, "id": 17, "legend": { @@ -1441,14 +1725,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1496,13 +1774,13 @@ "bars": false, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 51 }, "id": 20, "legend": { @@ -1581,13 +1859,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 59 }, "id": 14, "legend": { @@ -1617,14 +1895,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1675,13 +1947,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 59 }, "id": 15, "legend": { @@ -1762,13 +2034,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 67 }, "id": 25, "legend": { @@ -1798,14 +2070,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -1856,13 +2122,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 67 }, "id": 22, "legend": { @@ -1943,13 +2209,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 75 }, "id": 21, "legend": { @@ -1979,14 +2245,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2037,13 +2297,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 75 }, "id": 26, "legend": { @@ -2124,13 +2384,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 83 }, "id": 23, "legend": { @@ -2160,14 +2420,8 @@ { "alias": "", "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value one' as metric, \"valueOne\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", + "rawSql": "SELECT $__timeEpoch(time), measurement, \"valueOne\", \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", "refId": "A" - }, - { - "alias": "", - "format": "time_series", - "rawSql": "SELECT $__timeEpoch(time), measurement || ' - value two' as metric, \"valueTwo\" FROM metric_values \nWHERE $__timeFilter(time) AND measurement in($metric) ORDER BY 1", - "refId": "B" } ], "thresholds": [], @@ -2218,13 +2472,13 @@ "bars": true, "dashLength": 10, "dashes": false, - "datasource": "${DS_POSTGRES_TEST}", + "datasource": "gdev-postgres-ds-tests", "fill": 1, "gridPos": { "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 83 }, "id": 24, "legend": { @@ -2304,22 +2558,49 @@ "refresh": false, "schemaVersion": 16, "style": "dark", - "tags": [], + "tags": [ + "gdev", + "postgres" + ], "templating": { "list": [ { "allValue": null, - "current": {}, - "datasource": "${DS_POSTGRES_TEST}", + "current": { + "selected": true, + "tags": [], + "text": "All", + "value": [ + "$__all" + ] + }, + "datasource": "gdev-postgres-ds-tests", "hide": 0, "includeAll": true, "label": "Metric", "multi": true, "name": "metric", - "options": [], + "options": [ + { + "selected": true, + "text": "All", + "value": "$__all" + }, + { + "selected": false, + "text": "Metric A", + "value": "Metric A" + }, + { + "selected": false, + "text": "Metric B", + "value": "Metric B" + } + ], "query": "SELECT DISTINCT measurement FROM metric_values", - "refresh": 1, + "refresh": 0, "regex": "", + "skipUrlSync": false, "sort": 1, "tagValuesQuery": "", "tags": [], @@ -2372,6 +2653,7 @@ ], "query": "1s,10s,30s,1m,5m,10m", "refresh": 2, + "skipUrlSync": false, "type": "interval" } ] @@ -2406,7 +2688,7 @@ ] }, "timezone": "", - "title": "Postgres Data Source Test", + "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 14 + "version": 1 } \ No newline at end of file diff --git a/devenv/dev-dashboards/panel_tests_graph.json b/devenv/dev-dashboards/panel_tests_graph.json new file mode 100644 index 00000000000..8a1770f0fa6 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_graph.json @@ -0,0 +1,1558 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "no_data_points", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "No Data Points Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "datapoints_outside_range", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Datapoints Outside Range Warning", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Random walk series", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 7 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": "2s", + "timeShift": null, + "title": "Millisecond res x-axis and tooltip", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Just verify that the tooltip time has millisecond resolution ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 6, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 9, + "w": 16, + "x": 0, + "y": 14 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "yaxis": 2 + } + ], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "2000,3000,4000,1000,3000,10000", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "2 yaxis and axis labels", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "percent", + "label": "Perecent", + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": "Pressure", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Verify that axis labels look ok", + "editable": true, + "error": false, + "gridPos": { + "h": 9, + "w": 8, + "x": 16, + "y": 14 + }, + "id": 7, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 8, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value connected", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 10, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "null value null as zero", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 13, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 30 + }, + "id": 9, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,10,20,30,40,40,40,100,10,20,20", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stacking value ontop of nulls", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Stacking values on top of nulls, should treat the null values as zero. ", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 30 + }, + "id": 14, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 16, + "x": 0, + "y": 37 + }, + "id": 12, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "B-series", + "zindex": -3 + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "alias": "", + "hide": false, + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + }, + { + "alias": "", + "hide": false, + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Stacking all series null segment", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "content": "Stacking when all values are null should leave a gap in the graph", + "editable": true, + "error": false, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 37 + }, + "id": 15, + "links": [], + "mode": "markdown", + "title": "", + "type": "text" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 20, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table Single Series Should Take Minimum Height", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 51 + }, + "id": 16, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 51 + }, + "id": 17, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "F", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "G", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "I", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "J", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table Should Scroll", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 58 + }, + "id": 18, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "decimals": 3, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 58 + }, + "id": 19, + "legend": { + "alignAsTable": true, + "avg": true, + "current": true, + "max": true, + "min": true, + "rightSide": true, + "show": true, + "total": true, + "values": true + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "C", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "D", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "E", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "F", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "G", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "H", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "I", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "J", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "K", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + }, + { + "refId": "L", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeShift": null, + "title": "Legend Table No Scroll Visible", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "revision": 8, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Graph", + "uid": "5SdHCadmz", + "version": 3 +} diff --git a/devenv/dev-dashboards/panel_tests_singlestat.json b/devenv/dev-dashboards/panel_tests_singlestat.json new file mode 100644 index 00000000000..2d69f27bcb6 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_singlestat.json @@ -0,0 +1,574 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 0 + }, + "id": 2, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "postfix", + "postfixFontSize": "50%", + "prefix": "prefix", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,3,4,5" + } + ], + "thresholds": "5,10", + "title": "prefix 3 ms (green) postfixt + sparkline", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorPrefix": false, + "colorValue": true, + "colors": [ + "#d44a3a", + "rgba(237, 129, 40, 0.89)", + "#299c46" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 0 + }, + "id": 3, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,3,4,5" + } + ], + "thresholds": "5,10", + "title": "3 ms (red) + full height sparkline", + "type": "singlestat", + "valueFontSize": "200%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": true, + "colorPrefix": false, + "colorValue": false, + "colors": [ + "#d44a3a", + "rgba(237, 129, 40, 0.89)", + "#299c46" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 100, + "minValue": 0, + "show": false, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 0 + }, + "id": 4, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,2,3,4,5" + } + ], + "thresholds": "5,10", + "title": "3 ms + red background", + "type": "singlestat", + "valueFontSize": "200%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "avg" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorPrefix": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 150, + "minValue": 0, + "show": true, + "thresholdLabels": true, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 5, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "10,20,80" + } + ], + "thresholds": "81,90", + "title": "80 ms green gauge, thresholds 81, 90", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorPrefix": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 150, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": true + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 6, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "10,20,80" + } + ], + "thresholds": "81,90", + "title": "80 ms green gauge, thresholds 81, 90, no labels", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorPrefix": false, + "colorValue": true, + "colors": [ + "#299c46", + "rgba(237, 129, 40, 0.89)", + "#d44a3a" + ], + "datasource": "gdev-testdata", + "decimals": null, + "description": "", + "format": "ms", + "gauge": { + "maxValue": 150, + "minValue": 0, + "show": true, + "thresholdLabels": false, + "thresholdMarkers": false + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 7, + "interval": null, + "links": [], + "mappingType": 1, + "mappingTypes": [ + { + "name": "value to text", + "value": 1 + }, + { + "name": "range to text", + "value": 2 + } + ], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "rangeMaps": [ + { + "from": "null", + "text": "N/A", + "to": "null" + } + ], + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": true, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "tableColumn": "", + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "10,20,80" + } + ], + "thresholds": "81,90", + "title": "80 ms green gauge, thresholds 81, 90, no markers or labels", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + } + ], + "refresh": false, + "revision": 8, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Singlestat", + "uid": "singlestat", + "version": 14 +} diff --git a/devenv/dev-dashboards/panel_tests_table.json b/devenv/dev-dashboards/panel_tests_table.json new file mode 100644 index 00000000000..8337e9cd746 --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_table.json @@ -0,0 +1,453 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 11, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 3, + "links": [], + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "server1", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0,20,10" + }, + { + "alias": "server2", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "Time series to rows (2 pages)", + "transform": "timeseries_to_rows", + "type": "table" + }, + { + "columns": [ + { + "text": "Avg", + "value": "avg" + }, + { + "text": "Max", + "value": "max" + }, + { + "text": "Current", + "value": "current" + } + ], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 11, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "links": [], + "pageSize": 10, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "server1", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0,20,10" + }, + { + "alias": "server2", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0" + } + ], + "title": "Time series aggregations", + "transform": "timeseries_aggregations", + "type": "table" + }, + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 11 + }, + "id": 5, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "row", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "/Color/", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "ColorValue", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0,20,10" + } + ], + "title": "color row by threshold", + "transform": "timeseries_to_columns", + "type": "table" + }, + { + "columns": [], + "datasource": "gdev-testdata", + "fontSize": "100%", + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 2, + "links": [], + "pageSize": null, + "scroll": true, + "showHeader": true, + "sort": { + "col": 0, + "desc": true + }, + "styles": [ + { + "alias": "Time", + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "pattern": "Time", + "type": "date" + }, + { + "alias": "", + "colorMode": "cell", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorCell", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "currencyUSD" + }, + { + "alias": "", + "colorMode": "value", + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "dateFormat": "YYYY-MM-DD HH:mm:ss", + "decimals": 2, + "mappingType": 1, + "pattern": "ColorValue", + "thresholds": [ + "5", + "10" + ], + "type": "number", + "unit": "Bps" + }, + { + "alias": "", + "colorMode": null, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "decimals": 2, + "pattern": "/.*/", + "thresholds": [], + "type": "number", + "unit": "short" + } + ], + "targets": [ + { + "alias": "ColorValue", + "expr": "", + "format": "table", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0,20,10" + }, + { + "alias": "ColorCell", + "refId": "B", + "scenarioId": "csv_metric_values", + "stringInput": "5,1,2,3,4,5,10,20" + } + ], + "title": "Column style thresholds & units", + "transform": "timeseries_to_columns", + "type": "table" + } + ], + "refresh": false, + "revision": 8, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "Panel Tests - Table", + "uid": "pttable", + "version": 1 +} diff --git a/public/app/plugins/app/testdata/dashboards/alerts.json b/devenv/dev-dashboards/testdata_alerts.json similarity index 98% rename from public/app/plugins/app/testdata/dashboards/alerts.json rename to devenv/dev-dashboards/testdata_alerts.json index 159df0f458b..8c2edebf155 100644 --- a/public/app/plugins/app/testdata/dashboards/alerts.json +++ b/devenv/dev-dashboards/testdata_alerts.json @@ -1,6 +1,6 @@ { "revision": 2, - "title": "TestData - Alerts", + "title": "Alerting with TestData", "tags": [ "grafana-test" ], @@ -48,7 +48,7 @@ }, "aliasColors": {}, "bars": false, - "datasource": "Grafana TestData", + "datasource": "gdev-testdata", "editable": true, "error": false, "fill": 1, @@ -161,7 +161,7 @@ }, "aliasColors": {}, "bars": false, - "datasource": "Grafana TestData", + "datasource": "gdev-testdata", "editable": true, "error": false, "fill": 1, diff --git a/devenv/setup.sh b/devenv/setup.sh index 78dbfc1a366..cc71ecc71bf 100755 --- a/devenv/setup.sh +++ b/devenv/setup.sh @@ -1,4 +1,4 @@ -#/bin/bash +#!/bin/bash bulkDashboard() { @@ -7,11 +7,11 @@ bulkDashboard() { COUNTER=0 MAX=400 while [ $COUNTER -lt $MAX ]; do - jsonnet -o "dashboards/bulk-testing/dashboard${COUNTER}.json" -e "local bulkDash = import 'dashboards/bulk-testing/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" + jsonnet -o "bulk-dashboards/dashboard${COUNTER}.json" -e "local bulkDash = import 'bulk-dashboards/bulkdash.jsonnet'; bulkDash + { uid: 'uid-${COUNTER}', title: 'title-${COUNTER}' }" let COUNTER=COUNTER+1 done - ln -s -f -r ./dashboards/bulk-testing/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml + ln -s -f -r ./bulk-dashboards/bulk-dashboards.yaml ../conf/provisioning/dashboards/custom.yaml } requiresJsonnet() { @@ -22,31 +22,37 @@ requiresJsonnet() { fi } -defaultDashboards() { +devDashboards() { + echo -e "\xE2\x9C\x94 Setting up all dev dashboards using provisioning" ln -s -f ../../../devenv/dashboards.yaml ../conf/provisioning/dashboards/dev.yaml } -defaultDatasources() { - echo "setting up all default datasources using provisioning" +devDatasources() { + echo -e "\xE2\x9C\x94 Setting up all dev datasources using provisioning" ln -s -f ../../../devenv/datasources.yaml ../conf/provisioning/datasources/dev.yaml } usage() { - echo -e "install.sh\n\tThis script setups dev provision for datasources and dashboards" + echo -e "\n" echo "Usage:" echo " bulk-dashboards - create and provisioning 400 dashboards" echo " no args - provisiong core datasources and dev dashboards" } main() { + echo -e "------------------------------------------------------------------" + echo -e "This script setups provisioning for dev datasources and dashboards" + echo -e "------------------------------------------------------------------" + echo -e "\n" + local cmd=$1 if [[ $cmd == "bulk-dashboards" ]]; then bulkDashboard else - defaultDashboards - defaultDatasources + devDashboards + devDatasources fi if [[ -z "$cmd" ]]; then diff --git a/docker/blocks/nginx_proxy/Dockerfile b/docker/blocks/nginx_proxy/Dockerfile index 9ded20dfdda..04de507499d 100644 --- a/docker/blocks/nginx_proxy/Dockerfile +++ b/docker/blocks/nginx_proxy/Dockerfile @@ -1,3 +1,4 @@ FROM nginx:alpine -COPY nginx.conf /etc/nginx/nginx.conf \ No newline at end of file +COPY nginx.conf /etc/nginx/nginx.conf +COPY htpasswd /etc/nginx/htpasswd diff --git a/docker/blocks/nginx_proxy/htpasswd b/docker/blocks/nginx_proxy/htpasswd new file mode 100755 index 00000000000..e2c5eeeff7b --- /dev/null +++ b/docker/blocks/nginx_proxy/htpasswd @@ -0,0 +1,3 @@ +user1:$apr1$1odeeQb.$kwV8D/VAAGUDU7pnHuKoV0 +user2:$apr1$A2kf25r.$6S0kp3C7vIuixS5CL0XA9. +admin:$apr1$IWn4DoRR$E2ol7fS/dkI18eU4bXnBO1 diff --git a/docker/blocks/nginx_proxy/nginx.conf b/docker/blocks/nginx_proxy/nginx.conf index 18e27b3fb01..860d3d0b89f 100644 --- a/docker/blocks/nginx_proxy/nginx.conf +++ b/docker/blocks/nginx_proxy/nginx.conf @@ -13,7 +13,26 @@ http { listen 10080; location /grafana/ { + ################################################################ + # Enable these settings to test with basic auth and an auth proxy header + # the htpasswd file contains an admin user with password admin and + # user1: grafana and user2: grafana + ################################################################ + + # auth_basic "Restricted Content"; + # auth_basic_user_file /etc/nginx/htpasswd; + + ################################################################ + # To use the auth proxy header, set the following in custom.ini: + # [auth.proxy] + # enabled = true + # header_name = X-WEBAUTH-USER + # header_property = username + ################################################################ + + # proxy_set_header X-WEBAUTH-USER $remote_user; + proxy_pass http://localhost:3000/; } } -} \ No newline at end of file +} diff --git a/docker/blocks/openldap/ldap_dev.toml b/docker/blocks/openldap/ldap_dev.toml new file mode 100644 index 00000000000..e79771b57de --- /dev/null +++ b/docker/blocks/openldap/ldap_dev.toml @@ -0,0 +1,85 @@ +# To troubleshoot and get more log info enable ldap debug logging in grafana.ini +# [log] +# filters = ldap:debug + +[[servers]] +# Ldap server host (specify multiple hosts space separated) +host = "127.0.0.1" +# Default port is 389 or 636 if use_ssl = true +port = 389 +# Set to true if ldap server supports TLS +use_ssl = false +# Set to true if connect ldap server with STARTTLS pattern (create connection in insecure, then upgrade to secure connection with TLS) +start_tls = false +# set to true if you want to skip ssl cert validation +ssl_skip_verify = false +# set to the path to your root CA certificate or leave unset to use system defaults +# root_ca_cert = "/path/to/certificate.crt" + +# Search user bind dn +bind_dn = "cn=admin,dc=grafana,dc=org" +# Search user bind password +# If the password contains # or ; you have to wrap it with triple quotes. Ex """#password;""" +bind_password = 'grafana' + +# User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" +search_filter = "(cn=%s)" + +# An array of base dns to search through +search_base_dns = ["dc=grafana,dc=org"] + +# In POSIX LDAP schemas, without memberOf attribute a secondary query must be made for groups. +# This is done by enabling group_search_filter below. You must also set member_of= "cn" +# in [servers.attributes] below. + +# Users with nested/recursive group membership and an LDAP server that supports LDAP_MATCHING_RULE_IN_CHAIN +# can set group_search_filter, group_search_filter_user_attribute, group_search_base_dns and member_of +# below in such a way that the user's recursive group membership is considered. +# +# Nested Groups + Active Directory (AD) Example: +# +# AD groups store the Distinguished Names (DNs) of members, so your filter must +# recursively search your groups for the authenticating user's DN. For example: +# +# group_search_filter = "(member:1.2.840.113556.1.4.1941:=%s)" +# group_search_filter_user_attribute = "distinguishedName" +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] +# +# [servers.attributes] +# ... +# member_of = "distinguishedName" + +## Group search filter, to retrieve the groups of which the user is a member (only set if memberOf attribute is not available) +# group_search_filter = "(&(objectClass=posixGroup)(memberUid=%s))" +## Group search filter user attribute defines what user attribute gets substituted for %s in group_search_filter. +## Defaults to the value of username in [server.attributes] +## Valid options are any of your values in [servers.attributes] +## If you are using nested groups you probably want to set this and member_of in +## [servers.attributes] to "distinguishedName" +# group_search_filter_user_attribute = "distinguishedName" +## An array of the base DNs to search through for groups. Typically uses ou=groups +# group_search_base_dns = ["ou=groups,dc=grafana,dc=org"] + +# Specify names of the ldap attributes your ldap uses +[servers.attributes] +name = "givenName" +surname = "sn" +username = "cn" +member_of = "memberOf" +email = "email" + +# Map ldap groups to grafana org roles +[[servers.group_mappings]] +group_dn = "cn=admins,ou=groups,dc=grafana,dc=org" +org_role = "Admin" +# The Grafana organization database id, optional, if left out the default org (id 1) will be used +# org_id = 1 + +[[servers.group_mappings]] +group_dn = "cn=editors,ou=groups,dc=grafana,dc=org" +org_role = "Editor" + +[[servers.group_mappings]] +# If you want to match all (or no ldap groups) then you can use wildcard +group_dn = "*" +org_role = "Viewer" diff --git a/docker/blocks/openldap/notes.md b/docker/blocks/openldap/notes.md index 8de23d5ccf2..65155423616 100644 --- a/docker/blocks/openldap/notes.md +++ b/docker/blocks/openldap/notes.md @@ -14,12 +14,12 @@ After adding ldif files to `prepopulate`: ## Enabling LDAP in Grafana -The default `ldap.toml` file in `conf` has host set to `127.0.0.1` and port to set to 389 so all you need to do is enable it in the .ini file to get Grafana to use this block: +Copy the ldap_dev.toml file in this folder into your `conf` folder (it is gitignored already). To enable it in the .ini file to get Grafana to use this block: ```ini [auth.ldap] enabled = true -config_file = conf/ldap.toml +config_file = conf/ldap_dev.toml ; allow_sign_up = true ``` @@ -43,6 +43,3 @@ editors no groups ldap-viewer - - - diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index b3b4305a748..58046cafae4 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -130,7 +130,7 @@ There are a couple of configuration options which need to be set up in Grafana U Once these two properties are set, you can send the alerts to Kafka for further processing or throttling. -### All supported notifier +### All supported notifiers Name | Type |Support images -----|------------ | ------ @@ -148,6 +148,7 @@ Pushover | `pushover` | no Telegram | `telegram` | no Line | `line` | no Prometheus Alertmanager | `prometheus-alertmanager` | no +Microsoft Teams | `teams` | yes diff --git a/docs/sources/features/datasources/cloudwatch.md b/docs/sources/features/datasources/cloudwatch.md index d178c176602..7adc6ebe4fb 100644 --- a/docs/sources/features/datasources/cloudwatch.md +++ b/docs/sources/features/datasources/cloudwatch.md @@ -115,6 +115,8 @@ and `dimension keys/values`. In place of `region` you can specify `default` to use the default region configured in the datasource for the query, e.g. `metrics(AWS/DynamoDB, default)` or `dimension_values(default, ..., ..., ...)`. +Read more about the available dimensions in the [CloudWatch Metrics and Dimensions Reference](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CW_Support_For_AWS.html). + Name | Description ------- | -------- *regions()* | Returns a list of regions AWS provides their service. diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 31ce78f0bfe..80a2f9a828a 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -58,8 +58,8 @@ a time pattern for the index name or a wildcard. ### Elasticsearch version -Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. Currently only 2.x and 5.x -are supported. +Be sure to specify your Elasticsearch version in the version selection dropdown. This is very important as there are differences how queries are composed. +Currently the versions available is 2.x, 5.x and 5.6+ where 5.6+ means a version of 5.6 or higher, 6.3.2 for example. ### Min time interval A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example `1m` if your data is written every minute. @@ -115,7 +115,7 @@ The Elasticsearch data source supports two types of queries you can use in the * Query | Description ------------ | ------------- -*{"find": "fields", "type": "keyword"} | Returns a list of field names with the index type `keyword`. +*{"find": "fields", "type": "keyword"}* | Returns a list of field names with the index type `keyword`. *{"find": "terms", "field": "@hostname", "size": 1000}* | Returns a list of values for a field using term aggregation. Query will user current dashboard time range as time range for query. *{"find": "terms", "field": "@hostname", "query": ''}* | Returns a list of values for a field using term aggregation & and a specified lucene query filter. Query will use current dashboard time range as time range for query. diff --git a/docs/sources/features/datasources/mssql.md b/docs/sources/features/datasources/mssql.md index d4d5cc6d73e..da0c9581e99 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -81,10 +81,15 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m'[, fillvalue])* | Will be replaced by an expression usable in GROUP BY clause. Providing a *fillValue* of *NULL* or *floating value* will automatically fill empty series in timerange with that value.
For example, *CAST(ROUND(DATEDIFF(second, '1970-01-01', time_column)/300.0, 0) as bigint)\*300*. -*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. @@ -148,7 +153,8 @@ The resulting table panel: ## Time series queries -If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, tha name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. +If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must must have a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch in seconds. You may return a column named `metric` that is used as metric name for the value column. Any column except `time` and `metric` is treated as a value column. If you omit the `metric` column, the name of the value column will be the metric name. You may select multiple value columns, each will have its name as metric. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example database table:** diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index ce50053c7ea..afac746b050 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -64,10 +64,15 @@ Macro example | Description *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* *$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *cast(cast(UNIX_TIMESTAMP(dateColumn)/(300) as signed)*300 as signed),* -*$__timeGroup(dateColumn,'5m',0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. @@ -104,6 +109,7 @@ The resulting table panel: If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. Any column except `time` and `metric` is treated as a value column. You may return a column named `metric` that is used as metric name for the value column. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example with `metric` column:** diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index f9af60a2efc..1d195a01349 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -31,6 +31,7 @@ Name | Description *User* | Database user's login/username *Password* | Database user's password *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server. +*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+). ### Database User Permissions (Important!) @@ -60,11 +61,16 @@ Macro example | Description *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn BETWEEN '2017-04-21T05:01:17Z' AND '2017-04-21T05:06:17Z'* *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *'2017-04-21T05:01:17Z'* *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *'2017-04-21T05:06:17Z'* -*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300 AS time* -*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so all null values will be converted to the fill value (all null values would be set to zero using this example). +*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from dateColumn)/300)::bigint*300* +*$__timeGroup(dateColumn,'5m', 0)* | Same as above but with a fill parameter so missing points in that series will be added by grafana and 0 will be used as value. +*$__timeGroup(dateColumn,'5m', NULL)* | Same as above but NULL will be used as value for missing points. +*$__timeGroup(dateColumn,'5m', previous)* | Same as above but the previous value in that series will be used as fill value if no value has been seen yet NULL will be used (only available in Grafana 5.3+). +*$__timeGroupAlias(dateColumn,'5m')* | Will be replaced identical to $__timeGroup but with an added column alias (only available in Grafana 5.3+). *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183* *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783* *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183* +*$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+). +*$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+). We plan to add many more macros. If you have suggestions for what macros you would like to see, please [open an issue](https://github.com/grafana/grafana) in our GitHub repo. @@ -102,6 +108,7 @@ The resulting table panel: If you set `Format as` to `Time series`, for use in Graph panel for example, then the query must return a column named `time` that returns either a sql datetime or any numeric datatype representing unix epoch. Any column except `time` and `metric` is treated as a value column. You may return a column named `metric` that is used as metric name for the value column. +If you return multiple value columns and a column named `metric` then this column is used as prefix for the series name (only available in Grafana 5.3+). **Example with `metric` column:** @@ -285,4 +292,5 @@ datasources: password: "Password!" jsonData: sslmode: "disable" # disable/require/verify-ca/verify-full + timescaledb: false ``` diff --git a/docs/sources/features/datasources/prometheus.md b/docs/sources/features/datasources/prometheus.md index 4ff0baee108..611a3b4d9e2 100644 --- a/docs/sources/features/datasources/prometheus.md +++ b/docs/sources/features/datasources/prometheus.md @@ -75,6 +75,32 @@ Name | Description For details of *metric names*, *label names* and *label values* are please refer to the [Prometheus documentation](http://prometheus.io/docs/concepts/data_model/#metric-names-and-labels). + +#### Using interval and range variables + +> Support for `$__range`, `$__range_s` and `$__range_ms` only available from Grafana v5.3 + +It's possible to use some global built-in variables in query variables; `$__interval`, `$__interval_ms`, `$__range`, `$__range_s` and `$__range_ms`, see [Global built-in variables](/reference/templating/#global-built-in-variables) for more information. These can be convenient to use in conjunction with the `query_result` function when you need to filter variable queries since +`label_values` function doesn't support queries. + +Make sure to set the variable's `refresh` trigger to be `On Time Range Change` to get the correct instances when changing the time range on the dashboard. + +**Example usage:** + +Populate a variable with the the busiest 5 request instances based on average QPS over the time range shown in the dashboard: + +``` +Query: query_result(topk(5, sum(rate(http_requests_total[$__range])) by (instance))) +Regex: /"([^"]+)"/ +``` + +Populate a variable with the instances having a certain state over the time range shown in the dashboard, using the more precise `$__range_s`: + +``` +Query: query_result(max_over_time([${__range_s}s]) != ) +Regex: +``` + ### Using variables in queries There are two syntaxes: diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index b710a227a79..d3f8dd0ba63 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -54,7 +54,7 @@ We utilize a unit abstraction so that Grafana looks great on all screens both sm > Note: With MaxDataPoint functionality, Grafana can show you the perfect amount of datapoints no matter your resolution or time-range. -Utilize the [Repeating Row functionality](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected. +Utilize the [Repeating Rows functionality](/reference/templating/#repeating-rows) to dynamically create or remove entire Rows (that can be filled with Panels), based on the Template variables selected. Rows can be collapsed by clicking on the Row Title. If you save a Dashboard with a Row collapsed, it will save in that state and will not preload those graphs until the row is expanded. @@ -72,7 +72,7 @@ Panels like the [Graph](/reference/graph/) panel allow you to graph as many metr Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/templating/) variable strings within the panel configuration (including queries to your Data Source configured via the Query Editor). -Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected. +Utilize the [Repeating Panel](/reference/templating/#repeating-panels) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#repeating-panels) selected. The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift). diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index e4fe0dad3ff..80b6e283be3 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -59,7 +59,6 @@ Content-Type: application/json "panelId": 1, "name": "fire place sensor", "state": "alerting", - "message": "Someone is trying to break in through the fire place", "newStateDate": "2018-05-14T05:55:20+02:00", "evalDate": "0001-01-01T00:00:00Z", "evalData": null, diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index ea1bd7f2ef7..3df36894901 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -85,7 +85,7 @@ Status Codes: - **403** – Access denied - **412** – Precondition failed -The **412** status code is used for explaing that you cannot create the dashboard and why. +The **412** status code is used for explaining that you cannot create the dashboard and why. There can be different reasons for this: - The dashboard has been changed by someone else, `status=version-mismatch` diff --git a/docs/sources/http_api/folder.md b/docs/sources/http_api/folder.md index fb318ecf58e..e8845c3b125 100644 --- a/docs/sources/http_api/folder.md +++ b/docs/sources/http_api/folder.md @@ -223,7 +223,7 @@ Status Codes: - **404** – Folder not found - **412** – Precondition failed -The **412** status code is used for explaing that you cannot update the folder and why. +The **412** status code is used for explaining that you cannot update the folder and why. There can be different reasons for this: - The folder has been changed by someone else, `status=version-mismatch` diff --git a/docs/sources/http_api/playlist.md b/docs/sources/http_api/playlist.md new file mode 100644 index 00000000000..7c33900969b --- /dev/null +++ b/docs/sources/http_api/playlist.md @@ -0,0 +1,286 @@ ++++ +title = "Playlist HTTP API " +description = "Playlist Admin HTTP API" +keywords = ["grafana", "http", "documentation", "api", "playlist"] +aliases = ["/http_api/playlist/"] +type = "docs" +[menu.docs] +name = "Playlist" +parent = "http_api" ++++ + +# Playlist API + +## Search Playlist + +`GET /api/playlists` + +Get all existing playlist for the current organization using pagination + +**Example Request**: + +```bash +GET /api/playlists HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + + Querystring Parameters: + + These parameters are used as querystring parameters. + + - **query** - Limit response to playlist having a name like this value. + - **limit** - Limit response to *X* number of playlist. + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +[ + { + "id": 1, + "name": "my playlist", + "interval": "5m" + } +] +``` + +## Get one playlist + +`GET /api/playlists/:id` + +**Example Request**: + +```bash +GET /api/playlists/1 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +{ + "id" : 1, + "name": "my playlist", + "interval": "5m", + "orgId": "my org", + "items": [ + { + "id": 1, + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my third dasboard" + }, + { + "id": 2, + "playlistId": 1, + "type": "dashboard_by_tag", + "value": "myTag", + "order": 2, + "title":"my other dasboard" + } + ] +} +``` + +## Get Playlist items + +`GET /api/playlists/:id/items` + +**Example Request**: + +```bash +GET /api/playlists/1/items HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +[ + { + "id": 1, + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my third dasboard" + }, + { + "id": 2, + "playlistId": 1, + "type": "dashboard_by_tag", + "value": "myTag", + "order": 2, + "title":"my other dasboard" + } +] +``` + +## Get Playlist dashboards + +`GET /api/playlists/:id/dashboards` + +**Example Request**: + +```bash +GET /api/playlists/1/dashboards HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +[ + { + "id": 3, + "title": "my third dasboard", + "order": 1, + }, + { + "id": 5, + "title":"my other dasboard" + "order": 2, + + } +] +``` + +## Create a playlist + +`POST /api/playlists/` + +**Example Request**: + +```bash +PUT /api/playlists/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + { + "name": "my playlist", + "interval": "5m", + "items": [ + { + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my third dasboard" + }, + { + "type": "dashboard_by_tag", + "value": "myTag", + "order": 2, + "title":"my other dasboard" + } + ] + } +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json + { + "id": 1, + "name": "my playlist", + "interval": "5m" + } +``` + +## Update a playlist + +`PUT /api/playlists/:id` + +**Example Request**: + +```bash +PUT /api/playlists/1 HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk + { + "name": "my playlist", + "interval": "5m", + "items": [ + { + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my third dasboard" + }, + { + "playlistId": 1, + "type": "dashboard_by_tag", + "value": "myTag", + "order": 2, + "title":"my other dasboard" + } + ] + } +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +{ + "id" : 1, + "name": "my playlist", + "interval": "5m", + "orgId": "my org", + "items": [ + { + "id": 1, + "playlistId": 1, + "type": "dashboard_by_id", + "value": "3", + "order": 1, + "title":"my third dasboard" + }, + { + "id": 2, + "playlistId": 1, + "type": "dashboard_by_tag", + "value": "myTag", + "order": 2, + "title":"my other dasboard" + } + ] +} +``` + +## Delete a playlist + +`DELETE /api/playlists/:id` + +**Example Request**: + +```bash +DELETE /api/playlists/1 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```json +HTTP/1.1 200 +Content-Type: application/json +{} +``` diff --git a/docs/sources/http_api/user.md b/docs/sources/http_api/user.md index 134c1842851..b9047187b2d 100644 --- a/docs/sources/http_api/user.md +++ b/docs/sources/http_api/user.md @@ -363,6 +363,39 @@ Content-Type: application/json ] ``` +## Teams that the actual User is member of + +`GET /api/user/teams` + +Return a list of all teams that the current user is member of. + +**Example Request**: + +```http +GET /api/user/teams HTTP/1.1 +Accept: application/json +Content-Type: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +``` + +**Example Response**: + +```http +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 1, + "orgId": 1, + "name": "MyTestTeam", + "email": "", + "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398", + "memberCount": 1 + } +] +``` + ## Star a dashboard `POST /api/user/stars/dashboard/:dashboardId` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 668a44fcb2b..4b14829b689 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -15,6 +15,8 @@ weight = 1 The Grafana back-end has a number of configuration options that can be specified in a `.ini` configuration file or specified using environment variables. +> **Note.** Grafana needs to be restarted for any configuration changes to take effect. + ## Comments In .ini Files Semicolons (the `;` char) are the standard way to comment out lines in a `.ini` file. @@ -82,7 +84,7 @@ command line in the init.d script or the systemd service file. ### temp_data_lifetime -How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours), +How long temporary images in `data` directory should be kept. Defaults to: `24h`. Supported modifiers: `h` (hours), `m` (minutes), for example: `168h`, `30m`, `10h30m`. Use `0` to never clean up temporary files. ### logs @@ -179,7 +181,7 @@ embedded database (included in the main Grafana binary). ### url -Use either URL or or the other fields below to configure the database +Use either URL or the other fields below to configure the database Example: `mysql://user:secret@host:port/database` ### type @@ -193,9 +195,9 @@ will be stored. ### host -Only applicable to MySQL or Postgres. Includes IP or hostname and port. +Only applicable to MySQL or Postgres. Includes IP or hostname and port or in case of unix sockets the path to it. For example, for MySQL running on the same host as Grafana: `host = -127.0.0.1:3306` +127.0.0.1:3306` or with unix sockets: `host = /var/run/mysqld/mysqld.sock` ### name @@ -296,6 +298,12 @@ Set to `true` to automatically add new users to the main organization (id 1). When set to `false`, new users will automatically cause a new organization to be created for that new user. +### auto_assign_org_id + +Set this value to automatically add new users to the provided org. +This requires `auto_assign_org` to be set to `true`. Please make sure +that this organization does already exists. + ### auto_assign_org_role The role new users will be assigned for the main organization (if the @@ -422,6 +430,108 @@ allowed_organizations = github google
+## [auth.gitlab] + +> Only available in Grafana v5.3+. + +You need to [create a GitLab OAuth +application](https://docs.gitlab.com/ce/integration/oauth_provider.html). +Choose a descriptive *Name*, and use the following *Redirect URI*: + +``` +https://grafana.example.com/login/gitlab +``` + +where `https://grafana.example.com` is the URL you use to connect to Grafana. +Adjust it as needed if you don't use HTTPS or if you use a different port; for +instance, if you access Grafana at `http://203.0.113.31:3000`, you should use + +``` +http://203.0.113.31:3000/login/gitlab +``` + +Finally, select *api* as the *Scope* and submit the form. Note that if you're +not going to use GitLab groups for authorization (i.e. not setting +`allowed_groups`, see below), you can select *read_user* instead of *api* as +the *Scope*, thus giving a more restricted access to your GitLab API. + +You'll get an *Application Id* and a *Secret* in return; we'll call them +`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this +section. + +Add the following to your Grafana configuration file to enable GitLab +authentication: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = false +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = +``` + +Restart the Grafana backend for your changes to take effect. + +If you use your own instance of GitLab instead of `gitlab.com`, adjust +`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` +hostname with your own. + +With `allow_sign_up` set to `false`, only existing users will be able to login +using their GitLab account, but with `allow_sign_up` set to `true`, *any* user +who can authenticate on GitLab will be able to login on your Grafana instance; +if you use the public `gitlab.com`, it means anyone in the world would be able +to login on your Grafana instance. + +You can can however limit access to only members of a given group or list of +groups by setting the `allowed_groups` option. + +### allowed_groups + +To limit access to authenticated users that are members of one or more [GitLab +groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` +to a comma- or space-separated list of groups. For instance, if you want to +only give access to members of the `example` group, set + + +```ini +allowed_groups = example +``` + +If you want to also give access to members of the subgroup `bar`, which is in +the group `foo`, set + +```ini +allowed_groups = example, foo/bar +``` + +Note that in GitLab, the group or subgroup name doesn't always match its +display name, especially if the display name contains spaces or special +characters. Make sure you always use the group or subgroup name as it appears +in the URL of the group or subgroup. + +Here's a complete example with `alloed_sign_up` enabled, and access limited to +the `example` and `foo/bar` groups: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = example, foo/bar +``` + +
+ ## [auth.google] First, you need to create a Google OAuth Client: @@ -689,9 +799,9 @@ session provider you have configured. - **file:** session file path, e.g. `data/sessions` - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` -- **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full -- **memcache:** ex: 127.0.0.1:11211 -- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana` +- **postgres:** ex: `user=a password=b host=localhost port=5432 dbname=c sslmode=verify-full` +- **memcache:** ex: `127.0.0.1:11211` +- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,prefix=grafana`. For unix socket, use for example: `network=unix,addr=/var/run/redis/redis.sock,pool_size=100,db=grafana` Postgres valid `sslmode` are `disable`, `require`, `verify-ca`, and `verify-full` (default). @@ -857,7 +967,7 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Url to where Grafana will send PUT request with images ### public_url -Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name. +Optional parameter. Url to send to users in notifications. If the string contains the sequence ${file}, it will be replaced with the uploaded filename. Otherwise, the file name will be appended to the path part of the url, leaving any query string unchanged. ### username basic auth username diff --git a/docs/sources/installation/docker.md b/docs/sources/installation/docker.md index 1f755625699..6bf25ad8232 100644 --- a/docs/sources/installation/docker.md +++ b/docs/sources/installation/docker.md @@ -38,6 +38,8 @@ The back-end web server has a number of configuration options. Go to the [Configuration]({{< relref "configuration.md" >}}) page for details on all those options. +> For any changes to `conf/grafana.ini` (or corresponding environment variables) to take effect you need to restart Grafana by restarting the Docker container. + ## Running a Specific Version of Grafana ```bash @@ -49,10 +51,13 @@ $ docker run \ grafana/grafana:5.1.0 ``` -## Running of the master branch +## Running the master branch -For every successful commit we publish a Grafana container to [`grafana/grafana`](https://hub.docker.com/r/grafana/grafana/tags/) and [`grafana/grafana-dev`](https://hub.docker.com/r/grafana/grafana-dev/tags/). In `grafana/grafana` container we will always overwrite the `master` tag with the latest version. In `grafana/grafana-dev` we will include -the git commit in the tag. If you run Grafana master in production we **strongly** recommend that you use the later since different machines might run different version of grafana if they pull the master tag at different times. +For every successful build of the master branch we update the `grafana/grafana:master` tag and create a new tag `grafana/grafana-dev:master-` with the hash of the git commit that was built. This means you can always get the latest version of Grafana. + +When running Grafana master in production we **strongly** recommend that you use the `grafana/grafana-dev:master-` tag as that will guarantee that you use a specific version of Grafana instead of whatever was the most recent commit at the time. + +For a list of available tags, check out [grafana/grafana](https://hub.docker.com/r/grafana/grafana/tags/) and [grafana/grafana-dev](https://hub.docker.com/r/grafana/grafana-dev/tags/). ## Installing Plugins for Grafana diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 85501e51d85..88cf40632db 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -23,8 +23,9 @@ specific configuration file (default: `/etc/grafana/ldap.toml`). ### Example config ```toml -# Set to true to log user information returned from LDAP -verbose_logging = false +# To troubleshoot and get more log info enable ldap debug logging in grafana.ini +# [log] +# filters = ldap:debug [[servers]] # Ldap server host (specify multiple hosts space separated) @@ -39,6 +40,9 @@ start_tls = false ssl_skip_verify = false # set to the path to your root CA certificate or leave unset to use system defaults # root_ca_cert = "/path/to/certificate.crt" +# Authentication against LDAP servers requiring client certificates +# client_cert = "/path/to/client.crt" +# client_key = "/path/to/client.key" # Search user bind dn bind_dn = "cn=admin,dc=grafana,dc=org" @@ -47,6 +51,7 @@ bind_dn = "cn=admin,dc=grafana,dc=org" bind_password = 'grafana' # User search filter, for example "(cn=%s)" or "(sAMAccountName=%s)" or "(uid=%s)" +# Allow login from email or username, example "(|(sAMAccountName=%s)(userPrincipalName=%s))" search_filter = "(cn=%s)" # An array of base dns to search through @@ -73,6 +78,8 @@ email = "email" [[servers.group_mappings]] group_dn = "cn=admins,dc=grafana,dc=org" org_role = "Admin" +# To make user an instance admin (Grafana Admin) uncomment line below +# grafana_admin = true # The Grafana organization database id, optional, if left out the default org (id 1) will be used. Setting this allows for multiple group_dn's to be assigned to the same org_role provided the org_id differs # org_id = 1 @@ -132,6 +139,10 @@ Users page, this change will be reset the next time the user logs in. If you change the LDAP groups of a user, the change will take effect the next time the user logs in. +### Grafana Admin +with a servers.group_mappings section you can set grafana_admin = true or false to sync Grafana Admin permission. A Grafana server admin has admin access over all orgs & +users. + ### Priority The first group mapping that an LDAP user is matched to will be used for the sync. If you have LDAP users that fit multiple mappings, the topmost mapping in the TOML config will be used. diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index a0b553594ce..08673404572 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -57,7 +57,7 @@ For this you need nodejs (v.6+). ```bash npm install -g yarn yarn install --pure-lockfile -npm run watch +yarn watch ``` ## Running Grafana Locally @@ -83,21 +83,18 @@ go get github.com/Unknwon/bra bra run ``` -You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass) +You'll also need to run `yarn watch` to watch for changes to the front-end (typescript, html, sass) ### Running tests -- You can run backend Golang tests using "go test ./pkg/...". -- Execute all frontend tests with "npm run test" +- You can run backend Golang tests using `go test ./pkg/...`. +- Execute all frontend tests with `yarn test` -Writing & watching frontend tests (we have two test runners) +Writing & watching frontend tests + +- Start watcher: `yarn jest` +- Jest will run all test files that end with the name ".test.ts" -- jest for all new tests that do not require browser context (React+more) - - Start watcher: `npm run jest` - - Jest will run all test files that end with the name ".jest.ts" -- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible). - - Start watcher: `npm run karma` - - Karma+Mocha runs all files that end with the name "_specs.ts". ## Creating optimized release packages diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index 8341b9770bd..7f86465312c 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -11,7 +11,7 @@ weight = 1 # Variables Variables allows for more interactive and dynamic dashboards. Instead of hard-coding things like server, application -and sensor name in you metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of +and sensor name in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard. {{< docs-imagebox img="/img/docs/v50/variables_dashboard.png" >}} @@ -273,29 +273,49 @@ The `$__timeFilter` is used in the MySQL data source. This variable is only available in the Singlestat panel and can be used in the prefix or suffix fields on the Options tab. The variable will be replaced with the series name or alias. +### The $__range Variable + +> Only available in Grafana v5.3+ + +Currently only supported for Prometheus data sources. This variable represents the range for the current dashboard. It is calculated by `to - from`. It has a millisecond and a second representation called `$__range_ms` and `$__range_s`. + ## Repeating Panels Template variables can be very useful to dynamically change your queries across a whole dashboard. If you want Grafana to dynamically create new panels or rows based on what values you have selected you can use the *Repeat* feature. -If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel or one row and have Grafana repeat that row -for every selected value. You find this option under the General tab in panel edit mode. Select the variable to repeat by, and a `min span`. -The `min span` controls how small Grafana will make the panels (if you have many values selected). Grafana will automatically adjust the width of -each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated panel. +If you have a variable with `Multi-value` or `Include all value` options enabled you can choose one panel and have Grafana repeat that panel +for every selected value. You find the *Repeat* feature under the *General tab* in panel edit mode. + +The `direction` controls how the panels will be arranged. + +By choosing `horizontal` the panels will be arranged side-by-side. Grafana will automatically adjust the width +of each repeated panel so that the whole row is filled. Currently, you cannot mix other panels on a row with a repeated +panel. Each panel will never be smaller that the provided `Min width` if you have many selected values. + +By choosing `vertical` the panels will be arranged from top to bottom in a column. The `Min width` doesn't have any effect in this case. The width of the repeated panels will be the same as of the first panel (the original template) being repeated. Only make changes to the first panel (the original template). To have the changes take effect on all panels you need to trigger a dynamic dashboard re-build. You can do this by either changing the variable value (that is the basis for the repeat) or reload the dashboard. ## Repeating Rows -This option requires you to open the row options view. Hover over the row left side to trigger the row menu, in this menu click `Row Options`. This -opens the row options view. Here you find a *Repeat* dropdown where you can select the variable to repeat by. +As seen above with the *Panels* you can also repeat *Rows* if you have variables set with `Multi-value` or +`Include all value` selection option. -### URL state +To enable this feature you need to first add a new *Row* using the *Add Panel* menu. Then by hovering the row title and +clicking on the cog button, you will access the `Row Options` configuration panel. You can then select the variable +you want to repeat the row for. + +It may be a good idea to use a variable in the row title as well. + +Example: [Repeated Rows Dashboard](http://play.grafana.org/dashboard/db/repeated-rows) + +## URL state Variable values are always synced to the URL using the syntax `var-=value`. -### Examples +## Examples - [Graphite Templated Dashboard](http://play.grafana.org/dashboard/db/graphite-templated-nested) - [Elasticsearch Templated Dashboard](http://play.grafana.org/dashboard/db/elasticsearch-templated) diff --git a/jest.config.js b/jest.config.js index 606465c9840..a5cd3416f75 100644 --- a/jest.config.js +++ b/jest.config.js @@ -13,7 +13,7 @@ module.exports = { "roots": [ "/public" ], - "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$", + "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", "moduleFileExtensions": [ "ts", "tsx", diff --git a/karma.conf.js b/karma.conf.js deleted file mode 100644 index 352e8e4e027..00000000000 --- a/karma.conf.js +++ /dev/null @@ -1,40 +0,0 @@ -var webpack = require('webpack'); -var path = require('path'); -var webpackTestConfig = require('./scripts/webpack/webpack.test.js'); - -module.exports = function(config) { - - 'use strict'; - - config.set({ - frameworks: ['mocha', 'expect', 'sinon'], - - // list of files / patterns to load in the browser - files: [ - { pattern: 'public/test/index.ts', watched: false } - ], - - preprocessors: { - 'public/test/index.ts': ['webpack', 'sourcemap'], - }, - - webpack: webpackTestConfig, - webpackMiddleware: { - stats: 'minimal', - }, - - // list of files to exclude - exclude: [], - reporters: ['dots'], - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['PhantomJS'], - captureTimeout: 20000, - singleRun: true, - // autoWatchBatchDelay: 1000, - // browserNoActivityTimeout: 60000, - }); - -}; diff --git a/package.json b/package.json index 3523b9eac6d..4c70f1ec345 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "es6-shim": "^0.35.3", "expect.js": "~0.2.0", "expose-loader": "^0.7.3", - "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", "fork-ts-checker-webpack-plugin": "^0.4.2", "gaze": "^1.1.2", @@ -45,10 +44,7 @@ "grunt-contrib-concat": "^1.0.1", "grunt-contrib-copy": "~1.0.0", "grunt-contrib-cssmin": "~1.0.2", - "grunt-contrib-jshint": "~1.1.0", "grunt-exec": "^1.0.1", - "grunt-jscs": "3.0.1", - "grunt-karma": "~2.0.0", "grunt-notify": "^0.4.5", "grunt-postcss": "^0.8.0", "grunt-sass": "^2.0.0", @@ -60,23 +56,16 @@ "html-webpack-plugin": "^3.2.0", "husky": "^0.14.3", "jest": "^22.0.4", - "jshint-stylish": "~2.2.1", - "karma": "1.7.0", - "karma-chrome-launcher": "~2.2.0", - "karma-expect": "~1.1.3", - "karma-mocha": "~1.3.0", - "karma-phantomjs-launcher": "1.0.4", - "karma-sinon": "^1.0.5", - "karma-sourcemap-loader": "^0.3.7", - "karma-webpack": "^3.0.0", "lint-staged": "^6.0.0", "load-grunt-tasks": "3.5.2", + "mini-css-extract-plugin": "^0.4.0", "mobx-react-devtools": "^4.2.15", "mocha": "^4.0.1", "ng-annotate-loader": "^0.6.1", - "ng-annotate-webpack-plugin": "^0.2.1-pre", + "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", "npm": "^5.4.2", + "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", "postcss-browser-reporter": "^0.5.0", "postcss-loader": "^2.0.6", @@ -92,9 +81,11 @@ "systemjs-plugin-css": "^0.1.36", "ts-jest": "^22.4.6", "ts-loader": "^4.3.0", + "tslib": "^1.9.3", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", "typescript": "^2.6.2", + "uglifyjs-webpack-plugin": "^1.2.7", "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", @@ -111,7 +102,6 @@ "test": "grunt test", "test:coverage": "grunt test --coverage=true", "lint": "tslint -c tslint.json --project tsconfig.json --type-check", - "karma": "grunt karma:dev", "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "precommit": "lint-staged && grunt precommit" @@ -148,22 +138,21 @@ "classnames": "^2.2.5", "clipboard": "^1.7.1", "d3": "^4.11.0", - "d3-scale-chromatic": "^1.1.1", + "d3-scale-chromatic": "^1.3.0", "eventemitter3": "^2.0.3", "file-saver": "^1.3.3", "immutable": "^3.8.2", "jquery": "^3.2.1", "lodash": "^4.17.10", - "mini-css-extract-plugin": "^0.4.0", "mobx": "^3.4.1", "mobx-react": "^4.3.5", "mobx-state-tree": "^1.3.1", "moment": "^2.22.2", "mousetrap": "^1.6.0", "mousetrap-global-bind": "^1.1.0", - "optimize-css-assets-webpack-plugin": "^4.0.2", "prismjs": "^1.6.0", "prop-types": "^15.6.0", + "rc-cascader": "^0.14.0", "react": "^16.2.0", "react-dom": "^16.2.0", "react-grid-layout": "0.16.6", @@ -185,7 +174,7 @@ "tether": "^1.4.0", "tether-drop": "https://github.com/torkelo/drop/tarball/master", "tinycolor2": "^1.4.1", - "uglifyjs-webpack-plugin": "^1.2.7" + "tslint-react": "^3.6.0" }, "resolutions": { "caniuse-db": "1.0.30000772" diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile new file mode 100644 index 00000000000..890d6a4fb11 --- /dev/null +++ b/packaging/docker/Dockerfile @@ -0,0 +1,52 @@ +FROM debian:stretch-slim + +ARG GRAFANA_TGZ="grafana-latest.linux-x64.tar.gz" + +RUN apt-get update && apt-get install -qq -y tar && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz + +RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana + +FROM debian:stretch-slim + +ARG GF_UID="472" +ARG GF_GID="472" + +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" \ + GF_PATHS_DATA="/var/lib/grafana" \ + GF_PATHS_HOME="/usr/share/grafana" \ + GF_PATHS_LOGS="/var/log/grafana" \ + GF_PATHS_PLUGINS="/var/lib/grafana/plugins" \ + GF_PATHS_PROVISIONING="/etc/grafana/provisioning" + +WORKDIR $GF_PATHS_HOME + +RUN apt-get update && apt-get install -qq -y libfontconfig ca-certificates && \ + apt-get autoremove -y && \ + rm -rf /var/lib/apt/lists/* + +COPY --from=0 /tmp/grafana "$GF_PATHS_HOME" + +RUN mkdir -p "$GF_PATHS_HOME/.aws" && \ + groupadd -r -g $GF_GID grafana && \ + useradd -r -u $GF_UID -g grafana grafana && \ + mkdir -p "$GF_PATHS_PROVISIONING/datasources" \ + "$GF_PATHS_PROVISIONING/dashboards" \ + "$GF_PATHS_LOGS" \ + "$GF_PATHS_PLUGINS" \ + "$GF_PATHS_DATA" && \ + cp "$GF_PATHS_HOME/conf/sample.ini" "$GF_PATHS_CONFIG" && \ + cp "$GF_PATHS_HOME/conf/ldap.toml" /etc/grafana/ldap.toml && \ + chown -R grafana:grafana "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" && \ + chmod 777 "$GF_PATHS_DATA" "$GF_PATHS_HOME/.aws" "$GF_PATHS_LOGS" "$GF_PATHS_PLUGINS" + +EXPOSE 3000 + +COPY ./run.sh /run.sh + +USER grafana +ENTRYPOINT [ "/run.sh" ] diff --git a/packaging/docker/README.md b/packaging/docker/README.md new file mode 100644 index 00000000000..cfb3c7248ef --- /dev/null +++ b/packaging/docker/README.md @@ -0,0 +1,43 @@ +# Grafana Docker image + +## Running your Grafana container + +Start your container binding the external port `3000`. + +```bash +docker run -d --name=grafana -p 3000:3000 grafana/grafana +``` + +Try it out, default admin user is admin/admin. + +## How to use the container + +Further documentation can be found at http://docs.grafana.org/installation/docker/ + +## Changelog + +### v5.1.5, v5.2.0-beta2 +* Fix: config keys ending with _FILE are not respected [#170](https://github.com/grafana/grafana-docker/issues/170) + +### v5.2.0-beta1 +* Support for Docker Secrets + +### v5.1.0 +* Major restructuring of the container +* Usage of `chown` removed +* File permissions incompatibility with previous versions + * user id changed from 104 to 472 + * group id changed from 107 to 472 +* Runs as the grafana user by default (instead of root) +* All default volumes removed + +### v4.2.0 +* Plugins are now installed into ${GF_PATHS_PLUGINS} +* Building the container now requires a full url to the deb package instead of just version +* Fixes bug caused by installing multiple plugins + +### v4.0.0-beta2 +* Plugins dir (`/var/lib/grafana/plugins`) is no longer a separate volume + +### v3.1.1 +* Make it possible to install specific plugin version https://github.com/grafana/grafana-docker/issues/59#issuecomment-260584026 diff --git a/packaging/docker/build-deploy.sh b/packaging/docker/build-deploy.sh new file mode 100755 index 00000000000..ac3226a4a61 --- /dev/null +++ b/packaging/docker/build-deploy.sh @@ -0,0 +1,13 @@ +#!/bin/sh +set -e + +_grafana_version=$1 +./build.sh "$_grafana_version" +docker login -u "$DOCKER_USER" -p "$DOCKER_PASS" + +./push_to_docker_hub.sh "$_grafana_version" + +if echo "$_grafana_version" | grep -q "^master-"; then + apk add --no-cache curl + ./deploy_to_k8s.sh "grafana/grafana-dev:$_grafana_version" +fi diff --git a/packaging/docker/build.sh b/packaging/docker/build.sh new file mode 100755 index 00000000000..c303c71cd5f --- /dev/null +++ b/packaging/docker/build.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +_grafana_tag=$1 + +# If the tag starts with v, treat this as a official release +if echo "$_grafana_tag" | grep -q "^v"; then + _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) + _docker_repo=${2:-grafana/grafana} +else + _grafana_version=$_grafana_tag + _docker_repo=${2:-grafana/grafana-dev} +fi + +echo "Building ${_docker_repo}:${_grafana_version}" + +docker build \ + --tag "${_docker_repo}:${_grafana_version}" \ + --no-cache=true . + +# Tag as 'latest' for official release; otherwise tag as grafana/grafana:master +if echo "$_grafana_tag" | grep -q "^v"; then + docker tag "${_docker_repo}:${_grafana_version}" "${_docker_repo}:latest" +else + docker tag "${_docker_repo}:${_grafana_version}" "grafana/grafana:master" +fi diff --git a/packaging/docker/custom/Dockerfile b/packaging/docker/custom/Dockerfile new file mode 100644 index 00000000000..79eba5f29e9 --- /dev/null +++ b/packaging/docker/custom/Dockerfile @@ -0,0 +1,16 @@ +ARG GRAFANA_VERSION="latest" + +FROM grafana/grafana:${GRAFANA_VERSION} + +USER grafana + +ARG GF_INSTALL_PLUGINS="" + +RUN if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then \ + OLDIFS=$IFS; \ + IFS=','; \ + for plugin in ${GF_INSTALL_PLUGINS}; do \ + IFS=$OLDIFS; \ + grafana-cli --pluginsDir "$GF_PATHS_PLUGINS" plugins install ${plugin}; \ + done; \ +fi diff --git a/packaging/docker/deploy_to_k8s.sh b/packaging/docker/deploy_to_k8s.sh new file mode 100755 index 00000000000..26cf88ef688 --- /dev/null +++ b/packaging/docker/deploy_to_k8s.sh @@ -0,0 +1,6 @@ +#!/bin/sh + +curl -s --header "Content-Type: application/json" \ + --data "{\"build_parameters\": {\"CIRCLE_JOB\": \"deploy\", \"IMAGE_NAMES\": \"$1\"}}" \ + --request POST \ + https://circleci.com/api/v1.1/project/github/raintank/deployment_tools/tree/master?circle-token=$CIRCLE_TOKEN diff --git a/packaging/docker/push_to_docker_hub.sh b/packaging/docker/push_to_docker_hub.sh new file mode 100755 index 00000000000..526c216f8fa --- /dev/null +++ b/packaging/docker/push_to_docker_hub.sh @@ -0,0 +1,24 @@ +#!/bin/sh +set -e + +_grafana_tag=$1 + +# If the tag starts with v, treat this as a official release +if echo "$_grafana_tag" | grep -q "^v"; then + _grafana_version=$(echo "${_grafana_tag}" | cut -d "v" -f 2) + _docker_repo=${2:-grafana/grafana} +else + _grafana_version=$_grafana_tag + _docker_repo=${2:-grafana/grafana-dev} +fi + +echo "pushing ${_docker_repo}:${_grafana_version}" +docker push "${_docker_repo}:${_grafana_version}" + +if echo "$_grafana_tag" | grep -q "^v" && echo "$_grafana_tag" | grep -vq "beta"; then + echo "pushing ${_docker_repo}:latest" + docker push "${_docker_repo}:latest" +elif echo "$_grafana_tag" | grep -q "master"; then + echo "pushing grafana/grafana:master" + docker push grafana/grafana:master +fi diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh new file mode 100755 index 00000000000..bc001bdf90a --- /dev/null +++ b/packaging/docker/run.sh @@ -0,0 +1,88 @@ +#!/bin/bash -e + +PERMISSIONS_OK=0 + +if [ ! -r "$GF_PATHS_CONFIG" ]; then + echo "GF_PATHS_CONFIG='$GF_PATHS_CONFIG' is not readable." + PERMISSIONS_OK=1 +fi + +if [ ! -w "$GF_PATHS_DATA" ]; then + echo "GF_PATHS_DATA='$GF_PATHS_DATA' is not writable." + PERMISSIONS_OK=1 +fi + +if [ ! -r "$GF_PATHS_HOME" ]; then + echo "GF_PATHS_HOME='$GF_PATHS_HOME' is not readable." + PERMISSIONS_OK=1 +fi + +if [ $PERMISSIONS_OK -eq 1 ]; then + echo "You may have issues with file permissions, more information here: http://docs.grafana.org/installation/docker/#migration-from-a-previous-version-of-the-docker-container-to-5-1-or-later" +fi + +if [ ! -d "$GF_PATHS_PLUGINS" ]; then + mkdir "$GF_PATHS_PLUGINS" +fi + +if [ ! -z ${GF_AWS_PROFILES+x} ]; then + > "$GF_PATHS_HOME/.aws/credentials" + + for profile in ${GF_AWS_PROFILES}; do + access_key_varname="GF_AWS_${profile}_ACCESS_KEY_ID" + secret_key_varname="GF_AWS_${profile}_SECRET_ACCESS_KEY" + region_varname="GF_AWS_${profile}_REGION" + + if [ ! -z "${!access_key_varname}" -a ! -z "${!secret_key_varname}" ]; then + echo "[${profile}]" >> "$GF_PATHS_HOME/.aws/credentials" + echo "aws_access_key_id = ${!access_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + echo "aws_secret_access_key = ${!secret_key_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + if [ ! -z "${!region_varname}" ]; then + echo "region = ${!region_varname}" >> "$GF_PATHS_HOME/.aws/credentials" + fi + fi + done + + chmod 600 "$GF_PATHS_HOME/.aws/credentials" +fi + +# Convert all environment variables with names ending in __FILE into the content of +# the file that they point at and use the name without the trailing __FILE. +# This can be used to carry in Docker secrets. +for VAR_NAME in $(env | grep '^GF_[^=]\+__FILE=.\+' | sed -r "s/([^=]*)__FILE=.*/\1/g"); do + VAR_NAME_FILE="$VAR_NAME"__FILE + if [ "${!VAR_NAME}" ]; then + echo >&2 "ERROR: Both $VAR_NAME and $VAR_NAME_FILE are set (but are exclusive)" + exit 1 + fi + echo "Getting secret $VAR_NAME from ${!VAR_NAME_FILE}" + export "$VAR_NAME"="$(< "${!VAR_NAME_FILE}")" + unset "$VAR_NAME_FILE" +done + +export HOME="$GF_PATHS_HOME" + +if [ ! -z "${GF_INSTALL_PLUGINS}" ]; then + OLDIFS=$IFS + IFS=',' + for plugin in ${GF_INSTALL_PLUGINS}; do + IFS=$OLDIFS + if [[ $plugin =~ .*\;.* ]]; then + pluginUrl=$(echo "$plugin" | cut -d';' -f 1) + pluginWithoutUrl=$(echo "$plugin" | cut -d';' -f 2) + grafana-cli --pluginUrl "${pluginUrl}" --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${pluginWithoutUrl} + else + grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + fi + done +fi + +exec grafana-server \ + --homepath="$GF_PATHS_HOME" \ + --config="$GF_PATHS_CONFIG" \ + "$@" \ + cfg:default.log.mode="console" \ + cfg:default.paths.data="$GF_PATHS_DATA" \ + cfg:default.paths.logs="$GF_PATHS_LOGS" \ + cfg:default.paths.plugins="$GF_PATHS_PLUGINS" \ + cfg:default.paths.provisioning="$GF_PATHS_PROVISIONING" diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 9eba0e0d5b6..331beeef5e4 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -31,7 +31,7 @@ func TestAlertingApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 6590eb19ff2..08f3018c694 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -119,7 +119,7 @@ func TestAnnotationsApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/api.go b/pkg/api/api.go index 8870b9b095e..906481bbb8a 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -73,8 +73,7 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/", reqSignedIn, Index) r.Get("/dashboards/*", reqSignedIn, Index) - r.Get("/explore/", reqEditorRole, Index) - r.Get("/explore/*", reqEditorRole, Index) + r.Get("/explore", reqEditorRole, Index) r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/*", reqSignedIn, Index) @@ -121,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() { userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser)) userRoute.Post("/using/:id", Wrap(UserSetUsingOrg)) userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList)) + userRoute.Get("/teams", Wrap(GetSignedInUserTeamList)) userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard)) userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard)) diff --git a/pkg/api/dashboard_snapshot_test.go b/pkg/api/dashboard_snapshot_test.go index 5e7637a24e1..e58f2c4712d 100644 --- a/pkg/api/dashboard_snapshot_test.go +++ b/pkg/api/dashboard_snapshot_test.go @@ -39,7 +39,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) { return nil }) - teamResp := []*m.Team{} + teamResp := []*m.TeamDTO{} bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { query.Result = teamResp return nil diff --git a/pkg/api/dashboard_test.go b/pkg/api/dashboard_test.go index 50a2e314f5c..283a9b5f12c 100644 --- a/pkg/api/dashboard_test.go +++ b/pkg/api/dashboard_test.go @@ -61,7 +61,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) @@ -230,7 +230,7 @@ func TestDashboardApiEndpoint(t *testing.T) { }) bus.AddHandler("test", func(query *m.GetTeamsByUserQuery) error { - query.Result = []*m.Team{} + query.Result = []*m.TeamDTO{} return nil }) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 6ffefea991a..23dbb221d71 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -158,12 +158,26 @@ func UpdateDataSource(c *m.ReqContext, cmd m.UpdateDataSourceCommand) Response { } return Error(500, "Failed to update datasource", err) } - ds := convertModelToDtos(cmd.Result) + + query := m.GetDataSourceByIdQuery{ + Id: cmd.Id, + OrgId: c.OrgId, + } + + if err := bus.Dispatch(&query); err != nil { + if err == m.ErrDataSourceNotFound { + return Error(404, "Data source not found", nil) + } + return Error(500, "Failed to query datasources", err) + } + + dtos := convertModelToDtos(query.Result) + return JSON(200, util.DynMap{ "message": "Datasource updated", "id": cmd.Id, "name": cmd.Name, - "datasource": ds, + "datasource": dtos, }) } diff --git a/pkg/api/login.go b/pkg/api/login.go index 01fa71a6e44..632d04e37f1 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,7 +78,13 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { user := userQuery.Result // validate remember me cookie - if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login { + signingKey := user.Rands + user.Password + if len(signingKey) < 10 { + c.Logger.Error("Invalid user signingKey") + return false + } + + if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login { return false } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index c1b8ffe595e..f2bc79df7ad 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -52,7 +52,7 @@ func QueryMetrics(c *m.ReqContext, reqDto dtos.MetricRequest) Response { if res.Error != nil { res.ErrorString = res.Error.Error() resp.Message = res.ErrorString - statusCode = 500 + statusCode = 400 } } @@ -99,7 +99,7 @@ func GetTestDataRandomWalk(c *m.ReqContext) Response { timeRange := tsdb.NewTimeRange(from, to) request := &tsdb.TsdbQuery{TimeRange: timeRange} - dsInfo := &m.DataSource{Type: "grafana-testdata-datasource"} + dsInfo := &m.DataSource{Type: "testdata"} request.Queries = append(request.Queries, &tsdb.Query{ RefId: "A", IntervalMs: intervalMs, diff --git a/pkg/api/playlist.go b/pkg/api/playlist.go index a90b6425cb6..0963df7d4c4 100644 --- a/pkg/api/playlist.go +++ b/pkg/api/playlist.go @@ -160,6 +160,7 @@ func CreatePlaylist(c *m.ReqContext, cmd m.CreatePlaylistCommand) Response { func UpdatePlaylist(c *m.ReqContext, cmd m.UpdatePlaylistCommand) Response { cmd.OrgId = c.OrgId + cmd.Id = c.ParamsInt64(":id") if err := bus.Dispatch(&cmd); err != nil { return Error(500, "Failed to save playlist", err) diff --git a/pkg/api/pluginproxy/ds_proxy.go b/pkg/api/pluginproxy/ds_proxy.go index b420398f9a9..fb2cab9b9b1 100644 --- a/pkg/api/pluginproxy/ds_proxy.go +++ b/pkg/api/pluginproxy/ds_proxy.go @@ -203,6 +203,7 @@ func (proxy *DataSourceProxy) getDirector() func(req *http.Request) { req.Header.Del("X-Forwarded-Host") req.Header.Del("X-Forwarded-Port") req.Header.Del("X-Forwarded-Proto") + req.Header.Set("User-Agent", fmt.Sprintf("Grafana/%s", setting.BuildVersion)) // set X-Forwarded-For header if req.RemoteAddr != "" { @@ -319,9 +320,15 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) { SecureJsonData: proxy.ds.SecureJsonData.Decrypt(), } - routeURL, err := url.Parse(proxy.route.Url) + interpolatedURL, err := interpolateString(proxy.route.Url, data) if err != nil { - logger.Error("Error parsing plugin route url") + logger.Error("Error interpolating proxy url", "error", err) + return + } + + routeURL, err := url.Parse(interpolatedURL) + if err != nil { + logger.Error("Error parsing plugin route url", "error", err) return } diff --git a/pkg/api/pluginproxy/ds_proxy_test.go b/pkg/api/pluginproxy/ds_proxy_test.go index bb553b4d075..e6d05872787 100644 --- a/pkg/api/pluginproxy/ds_proxy_test.go +++ b/pkg/api/pluginproxy/ds_proxy_test.go @@ -49,6 +49,13 @@ func TestDSRouteRule(t *testing.T) { {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, }, }, + { + Path: "api/common", + Url: "{{.JsonData.dynamicUrl}}", + Headers: []plugins.AppPluginRouteHeader{ + {Name: "x-header", Content: "my secret {{.SecureJsonData.key}}"}, + }, + }, }, } @@ -57,7 +64,8 @@ func TestDSRouteRule(t *testing.T) { ds := &m.DataSource{ JsonData: simplejson.NewFromAny(map[string]interface{}{ - "clientId": "asd", + "clientId": "asd", + "dynamicUrl": "https://dynamic.grafana.com", }), SecureJsonData: map[string][]byte{ "key": key, @@ -83,6 +91,17 @@ func TestDSRouteRule(t *testing.T) { }) }) + Convey("When matching route path and has dynamic url", func() { + proxy := NewDataSourceProxy(ds, plugin, ctx, "api/common/some/method") + proxy.route = plugin.Routes[3] + proxy.applyRoute(req) + + Convey("should add headers and interpolate the url", func() { + So(req.URL.String(), ShouldEqual, "https://dynamic.grafana.com/some/method") + So(req.Header.Get("x-header"), ShouldEqual, "my secret 123") + }) + }) + Convey("Validating request", func() { Convey("plugin route with valid role", func() { proxy := NewDataSourceProxy(ds, plugin, ctx, "api/v4/some/method") @@ -212,20 +231,21 @@ func TestDSRouteRule(t *testing.T) { }) Convey("When proxying graphite", func() { + setting.BuildVersion = "5.3.0" plugin := &plugins.DataSourcePlugin{} ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE} ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "/render") + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + So(err, ShouldBeNil) - requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL} - - proxy.getDirector()(&req) + proxy.getDirector()(req) Convey("Can translate request url and path", func() { So(req.URL.Host, ShouldEqual, "graphite:8080") So(req.URL.Path, ShouldEqual, "/render") + So(req.Header.Get("User-Agent"), ShouldEqual, "Grafana/5.3.0") }) }) @@ -243,10 +263,10 @@ func TestDSRouteRule(t *testing.T) { ctx := &m.ReqContext{} proxy := NewDataSourceProxy(ds, plugin, ctx, "") - requestURL, _ := url.Parse("http://grafana.com/sub") - req := http.Request{URL: requestURL} + req, err := http.NewRequest(http.MethodGet, "http://grafana.com/sub", nil) + So(err, ShouldBeNil) - proxy.getDirector()(&req) + proxy.getDirector()(req) Convey("Should add db to url", func() { So(req.URL.Path, ShouldEqual, "/db/site/") diff --git a/pkg/api/team.go b/pkg/api/team.go index 9919305881b..ebb426c4c82 100644 --- a/pkg/api/team.go +++ b/pkg/api/team.go @@ -93,5 +93,6 @@ func GetTeamByID(c *m.ReqContext) Response { return Error(500, "Failed to get Team", err) } + query.Result.AvatarUrl = dtos.GetGravatarUrlWithDefault(query.Result.Email, query.Result.Name) return JSON(200, &query.Result) } diff --git a/pkg/api/team_test.go b/pkg/api/team_test.go index 0bf06d723c8..a1984288870 100644 --- a/pkg/api/team_test.go +++ b/pkg/api/team_test.go @@ -13,7 +13,7 @@ import ( func TestTeamApiEndpoint(t *testing.T) { Convey("Given two teams", t, func() { mockResult := models.SearchTeamQueryResult{ - Teams: []*models.SearchTeamDto{ + Teams: []*models.TeamDTO{ {Name: "team1"}, {Name: "team2"}, }, diff --git a/pkg/api/user.go b/pkg/api/user.go index 725c623575f..4b916202e65 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.UserId) } +// GET /api/user/teams +func GetSignedInUserTeamList(c *m.ReqContext) Response { + query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId} + + if err := bus.Dispatch(&query); err != nil { + return Error(500, "Failed to get user teams", err) + } + + for _, team := range query.Result { + team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name) + } + + return JSON(200, query.Result) +} + // GET /api/user/:id/orgs func GetUserOrgList(c *m.ReqContext) Response { return getUserOrgList(c.ParamsInt64(":id")) diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go index f5478ea8a2f..ed6b14725c0 100644 --- a/pkg/components/imguploader/webdavuploader.go +++ b/pkg/components/imguploader/webdavuploader.go @@ -9,6 +9,7 @@ import ( "net/http" "net/url" "path" + "strings" "time" "github.com/grafana/grafana/pkg/util" @@ -35,6 +36,16 @@ var netClient = &http.Client{ Transport: netTransport, } +func (u *WebdavUploader) PublicURL(filename string) string { + if strings.Contains(u.public_url, "${file}") { + return strings.Replace(u.public_url, "${file}", filename, -1) + } else { + publicURL, _ := url.Parse(u.public_url) + publicURL.Path = path.Join(publicURL.Path, filename) + return publicURL.String() + } +} + func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) { url, _ := url.Parse(u.url) filename := util.GetRandomString(20) + ".png" @@ -65,9 +76,7 @@ func (u *WebdavUploader) Upload(ctx context.Context, pa string) (string, error) } if u.public_url != "" { - publicURL, _ := url.Parse(u.public_url) - publicURL.Path = path.Join(publicURL.Path, filename) - return publicURL.String(), nil + return u.PublicURL(filename), nil } return url.String(), nil diff --git a/pkg/components/imguploader/webdavuploader_test.go b/pkg/components/imguploader/webdavuploader_test.go index 5a8abd0542d..0178c9cda6c 100644 --- a/pkg/components/imguploader/webdavuploader_test.go +++ b/pkg/components/imguploader/webdavuploader_test.go @@ -2,6 +2,7 @@ package imguploader import ( "context" + "net/url" "testing" . "github.com/smartystreets/goconvey/convey" @@ -26,3 +27,15 @@ func TestUploadToWebdav(t *testing.T) { So(path, ShouldStartWith, "http://publicurl:8888/webdav/") }) } + +func TestPublicURL(t *testing.T) { + Convey("Given a public URL with parameters, and no template", t, func() { + webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=") + parsed, _ := url.Parse(webdavUploader.PublicURL("fileyfile.png")) + So(parsed.Path, ShouldEndWith, "fileyfile.png") + }) + Convey("Given a public URL with parameters, and a template", t, func() { + webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://cloudycloud.me/s/DOIFDOMV/download?files=${file}") + So(webdavUploader.PublicURL("fileyfile.png"), ShouldEndWith, "fileyfile.png") + }) +} diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go index d6eaf9a975e..a421e3ebe0a 100644 --- a/pkg/login/ext_user.go +++ b/pkg/login/ext_user.go @@ -72,6 +72,13 @@ func UpsertUser(cmd *m.UpsertUserCommand) error { return err } + // Sync isGrafanaAdmin permission + if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin { + if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil { + return err + } + } + err = bus.Dispatch(&m.SyncTeamsCommand{ User: cmd.Result, ExternalUser: extUser, diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index bdf87b2db54..053778e8deb 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error { } } } + var clientCert tls.Certificate + if a.server.ClientCert != "" && a.server.ClientKey != "" { + clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey) + if err != nil { + return err + } + } for _, host := range strings.Split(a.server.Host, " ") { address := fmt.Sprintf("%s:%d", host, a.server.Port) if a.server.UseSSL { @@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error { ServerName: host, RootCAs: certPool, } + if len(clientCert.Certificate) > 0 { + tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert) + } if a.server.StartTLS { a.conn, err = ldap.Dial("tcp", address) if err == nil { @@ -175,6 +185,7 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo if ldapUser.isMemberOf(group.GroupDN) { extUser.OrgRoles[group.OrgId] = group.OrgRole + extUser.IsGrafanaAdmin = group.IsGrafanaAdmin } } @@ -190,18 +201,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo } // add/update user in grafana - userQuery := &m.UpsertUserCommand{ + upsertUserCmd := &m.UpsertUserCommand{ ReqContext: ctx, ExternalUser: extUser, SignupAllowed: setting.LdapAllowSignup, } - err := bus.Dispatch(userQuery) + err := bus.Dispatch(upsertUserCmd) if err != nil { return nil, err } - return userQuery.Result, nil + return upsertUserCmd.Result, nil } func (a *ldapAuther) serverBind() error { diff --git a/pkg/login/ldap_settings.go b/pkg/login/ldap_settings.go index 497d8725e29..7ebfbc79ba8 100644 --- a/pkg/login/ldap_settings.go +++ b/pkg/login/ldap_settings.go @@ -21,6 +21,8 @@ type LdapServerConf struct { StartTLS bool `toml:"start_tls"` SkipVerifySSL bool `toml:"ssl_skip_verify"` RootCACert string `toml:"root_ca_cert"` + ClientCert string `toml:"client_cert"` + ClientKey string `toml:"client_key"` BindDN string `toml:"bind_dn"` BindPassword string `toml:"bind_password"` Attr LdapAttributeMap `toml:"attributes"` @@ -44,9 +46,10 @@ type LdapAttributeMap struct { } type LdapGroupToOrgRole struct { - GroupDN string `toml:"group_dn"` - OrgId int64 `toml:"org_id"` - OrgRole m.RoleType `toml:"org_role"` + GroupDN string `toml:"group_dn"` + OrgId int64 `toml:"org_id"` + IsGrafanaAdmin *bool `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatability) + OrgRole m.RoleType `toml:"org_role"` } var LdapCfg LdapConfig diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 5080840704e..1cf98bd1e14 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -98,6 +98,10 @@ func TestLdapAuther(t *testing.T) { So(result.Login, ShouldEqual, "torkelo") }) + Convey("Should set isGrafanaAdmin to false by default", func() { + So(result.IsAdmin, ShouldBeFalse) + }) + }) }) @@ -223,8 +227,32 @@ func TestLdapAuther(t *testing.T) { So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN) So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1) }) + + Convey("Should not update permissions unless specified", func() { + So(err, ShouldBeNil) + So(sc.updateUserPermissionsCmd, ShouldBeNil) + }) }) + ldapAutherScenario("given ldap groups with grafana_admin=true", func(sc *scenarioContext) { + trueVal := true + + ldapAuther := NewLdapAuthenticator(&LdapServerConf{ + LdapGroups: []*LdapGroupToOrgRole{ + {GroupDN: "cn=admins", OrgId: 1, OrgRole: "Admin", IsGrafanaAdmin: &trueVal}, + }, + }) + + sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + MemberOf: []string{"cn=admins"}, + }) + + Convey("Should create user with admin set to true", func() { + So(err, ShouldBeNil) + So(sc.updateUserPermissionsCmd.IsGrafanaAdmin, ShouldBeTrue) + }) + }) }) Convey("When calling SyncUser", t, func() { @@ -332,6 +360,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { return nil }) + bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.UpdateUserPermissionsCommand) error { + sc.updateUserPermissionsCmd = cmd + return nil + }) + bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error { sc.getUserByAuthInfoQuery = cmd sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login} @@ -379,14 +412,15 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { } type scenarioContext struct { - getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery - getUserOrgListQuery *m.GetUserOrgListQuery - createUserCmd *m.CreateUserCommand - addOrgUserCmd *m.AddOrgUserCommand - updateOrgUserCmd *m.UpdateOrgUserCommand - removeOrgUserCmd *m.RemoveOrgUserCommand - updateUserCmd *m.UpdateUserCommand - setUsingOrgCmd *m.SetUsingOrgCommand + getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery + getUserOrgListQuery *m.GetUserOrgListQuery + createUserCmd *m.CreateUserCommand + addOrgUserCmd *m.AddOrgUserCommand + updateOrgUserCmd *m.UpdateOrgUserCommand + removeOrgUserCmd *m.RemoveOrgUserCommand + updateUserCmd *m.UpdateUserCommand + setUsingOrgCmd *m.SetUsingOrgCommand + updateUserPermissionsCmd *m.UpdateUserPermissionsCommand } func (sc *scenarioContext) userQueryReturns(user *m.User) { diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 4dd84c12151..a8d9f7308fa 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -44,6 +44,7 @@ var ( M_Alerting_Notification_Sent *prometheus.CounterVec M_Aws_CloudWatch_GetMetricStatistics prometheus.Counter M_Aws_CloudWatch_ListMetrics prometheus.Counter + M_Aws_CloudWatch_GetMetricData prometheus.Counter M_DB_DataSource_QueryById prometheus.Counter // Timers @@ -218,6 +219,12 @@ func init() { Namespace: exporterName, }) + M_Aws_CloudWatch_GetMetricData = prometheus.NewCounter(prometheus.CounterOpts{ + Name: "aws_cloudwatch_get_metric_data_total", + Help: "counter for getting metric data time series from aws", + Namespace: exporterName, + }) + M_DB_DataSource_QueryById = prometheus.NewCounter(prometheus.CounterOpts{ Name: "db_datasource_query_by_id_total", Help: "counter for getting datasource by id", @@ -307,6 +314,7 @@ func initMetricVars() { M_Alerting_Notification_Sent, M_Aws_CloudWatch_GetMetricStatistics, M_Aws_CloudWatch_ListMetrics, + M_Aws_CloudWatch_GetMetricData, M_DB_DataSource_QueryById, M_Alerting_Active_Alerts, M_StatTotal_Dashboards, diff --git a/pkg/models/models.go b/pkg/models/models.go index c2560021ee1..ba894ae591f 100644 --- a/pkg/models/models.go +++ b/pkg/models/models.go @@ -8,4 +8,5 @@ const ( TWITTER GENERIC GRAFANA_COM + GITLAB ) diff --git a/pkg/models/playlist.go b/pkg/models/playlist.go index 5c49bb9256c..c52da202293 100644 --- a/pkg/models/playlist.go +++ b/pkg/models/playlist.go @@ -63,7 +63,7 @@ type PlaylistDashboards []*PlaylistDashboard type UpdatePlaylistCommand struct { OrgId int64 `json:"-"` - Id int64 `json:"id" binding:"Required"` + Id int64 `json:"id"` Name string `json:"name" binding:"Required"` Interval string `json:"interval"` Items []PlaylistItemDTO `json:"items"` diff --git a/pkg/models/team.go b/pkg/models/team.go index 9c679a13394..61285db3a5f 100644 --- a/pkg/models/team.go +++ b/pkg/models/team.go @@ -49,13 +49,13 @@ type DeleteTeamCommand struct { type GetTeamByIdQuery struct { OrgId int64 Id int64 - Result *Team + Result *TeamDTO } type GetTeamsByUserQuery struct { OrgId int64 - UserId int64 `json:"userId"` - Result []*Team `json:"teams"` + UserId int64 `json:"userId"` + Result []*TeamDTO `json:"teams"` } type SearchTeamsQuery struct { @@ -68,7 +68,7 @@ type SearchTeamsQuery struct { Result SearchTeamQueryResult } -type SearchTeamDto struct { +type TeamDTO struct { Id int64 `json:"id"` OrgId int64 `json:"orgId"` Name string `json:"name"` @@ -78,8 +78,8 @@ type SearchTeamDto struct { } type SearchTeamQueryResult struct { - TotalCount int64 `json:"totalCount"` - Teams []*SearchTeamDto `json:"teams"` - Page int `json:"page"` - PerPage int `json:"perPage"` + TotalCount int64 `json:"totalCount"` + Teams []*TeamDTO `json:"teams"` + Page int `json:"page"` + PerPage int `json:"perPage"` } diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go index 162a4d867a9..28189005737 100644 --- a/pkg/models/user_auth.go +++ b/pkg/models/user_auth.go @@ -13,14 +13,15 @@ type UserAuth struct { } type ExternalUserInfo struct { - AuthModule string - AuthId string - UserId int64 - Email string - Login string - Name string - Groups []string - OrgRoles map[int64]RoleType + AuthModule string + AuthId string + UserId int64 + Email string + Login string + Name string + Groups []string + OrgRoles map[int64]RoleType + IsGrafanaAdmin *bool // This is a pointer to know if we should sync this or not (nil = ignore sync) } // --------------------- diff --git a/pkg/plugins/datasource_plugin.go b/pkg/plugins/datasource_plugin.go index 2fec6acbf54..ff44805e35f 100644 --- a/pkg/plugins/datasource_plugin.go +++ b/pkg/plugins/datasource_plugin.go @@ -17,11 +17,14 @@ import ( plugin "github.com/hashicorp/go-plugin" ) +// DataSourcePlugin contains all metadata about a datasource plugin type DataSourcePlugin struct { FrontendPluginBase Annotations bool `json:"annotations"` Metrics bool `json:"metrics"` Alerting bool `json:"alerting"` + Explore bool `json:"explore"` + Logs bool `json:"logs"` QueryOptions map[string]bool `json:"queryOptions,omitempty"` BuiltIn bool `json:"builtIn,omitempty"` Mixed bool `json:"mixed,omitempty"` diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 07212746f7e..f4e0a0f434f 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,7 +3,6 @@ package alerting import ( "errors" "fmt" - "time" "golang.org/x/sync/errgroup" @@ -81,7 +80,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { renderOpts := rendering.Opts{ Width: 1000, Height: 500, - Timeout: time.Second * 30, + Timeout: alertTimeout / 2, OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, } diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index a8139b62726..c1dadba414d 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -58,7 +58,7 @@ func init() { data-placement="right"> - Provide a bot token to use the Slack file.upload API (starts with "xoxb") + Provide a bot token to use the Slack file.upload API (starts with "xoxb"). Specify #channel-name or @username in Recipient for this to work `, diff --git a/pkg/services/guardian/guardian.go b/pkg/services/guardian/guardian.go index cfd8f5c3a6e..7506338c5f0 100644 --- a/pkg/services/guardian/guardian.go +++ b/pkg/services/guardian/guardian.go @@ -30,7 +30,7 @@ type dashboardGuardianImpl struct { dashId int64 orgId int64 acl []*m.DashboardAclInfoDTO - groups []*m.Team + teams []*m.TeamDTO log log.Logger } @@ -186,15 +186,15 @@ func (g *dashboardGuardianImpl) GetAcl() ([]*m.DashboardAclInfoDTO, error) { return g.acl, nil } -func (g *dashboardGuardianImpl) getTeams() ([]*m.Team, error) { - if g.groups != nil { - return g.groups, nil +func (g *dashboardGuardianImpl) getTeams() ([]*m.TeamDTO, error) { + if g.teams != nil { + return g.teams, nil } query := m.GetTeamsByUserQuery{OrgId: g.orgId, UserId: g.user.UserId} err := bus.Dispatch(&query) - g.groups = query.Result + g.teams = query.Result return query.Result, err } diff --git a/pkg/services/guardian/guardian_util_test.go b/pkg/services/guardian/guardian_util_test.go index 3d839e71b74..d85548ecb8c 100644 --- a/pkg/services/guardian/guardian_util_test.go +++ b/pkg/services/guardian/guardian_util_test.go @@ -19,7 +19,7 @@ type scenarioContext struct { givenUser *m.SignedInUser givenDashboardID int64 givenPermissions []*m.DashboardAclInfoDTO - givenTeams []*m.Team + givenTeams []*m.TeamDTO updatePermissions []*m.DashboardAcl expectedFlags permissionFlags callerFile string @@ -84,11 +84,11 @@ func permissionScenario(desc string, dashboardID int64, sc *scenarioContext, per return nil }) - teams := []*m.Team{} + teams := []*m.TeamDTO{} for _, p := range permissions { if p.TeamId > 0 { - teams = append(teams, &m.Team{Id: p.TeamId}) + teams = append(teams, &m.TeamDTO{Id: p.TeamId}) } } diff --git a/pkg/services/notifications/notifications.go b/pkg/services/notifications/notifications.go index fcefa91243d..769fdd06fd0 100644 --- a/pkg/services/notifications/notifications.go +++ b/pkg/services/notifications/notifications.go @@ -98,8 +98,6 @@ func (ns *NotificationService) Run(ctx context.Context) error { return ctx.Err() } } - - return nil } func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { diff --git a/pkg/services/provisioning/datasources/config_reader.go b/pkg/services/provisioning/datasources/config_reader.go index 4b8931f0ed3..b2930c2b679 100644 --- a/pkg/services/provisioning/datasources/config_reader.go +++ b/pkg/services/provisioning/datasources/config_reader.go @@ -83,7 +83,7 @@ func (cr *configReader) parseDatasourceConfig(path string, file os.FileInfo) (*D } func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error { - defaultCount := 0 + defaultCount := map[int64]int{} for i := range datasources { if datasources[i].Datasources == nil { continue @@ -95,8 +95,8 @@ func validateDefaultUniqueness(datasources []*DatasourcesAsConfig) error { } if ds.IsDefault { - defaultCount++ - if defaultCount > 1 { + defaultCount[ds.OrgId] = defaultCount[ds.OrgId] + 1 + if defaultCount[ds.OrgId] > 1 { return ErrInvalidConfigToManyDefault } } diff --git a/pkg/services/provisioning/datasources/config_reader_test.go b/pkg/services/provisioning/datasources/config_reader_test.go index 2e407dbe4de..07c8d68e75c 100644 --- a/pkg/services/provisioning/datasources/config_reader_test.go +++ b/pkg/services/provisioning/datasources/config_reader_test.go @@ -19,6 +19,7 @@ var ( allProperties = "testdata/all-properties" versionZero = "testdata/version-0" brokenYaml = "testdata/broken-yaml" + multipleOrgsWithDefault = "testdata/multiple-org-default" fakeRepo *fakeRepository ) @@ -73,6 +74,19 @@ func TestDatasourceAsConfig(t *testing.T) { }) }) + Convey("Multiple datasources in different organizations with isDefault in each organization", func() { + dc := newDatasourceProvisioner(logger) + err := dc.applyChanges(multipleOrgsWithDefault) + Convey("should not raise error", func() { + So(err, ShouldBeNil) + So(len(fakeRepo.inserted), ShouldEqual, 4) + So(fakeRepo.inserted[0].IsDefault, ShouldBeTrue) + So(fakeRepo.inserted[0].OrgId, ShouldEqual, 1) + So(fakeRepo.inserted[2].IsDefault, ShouldBeTrue) + So(fakeRepo.inserted[2].OrgId, ShouldEqual, 2) + }) + }) + Convey("Two configured datasource and purge others ", func() { Convey("two other datasources in database", func() { fakeRepo.loadAll = []*models.DataSource{ diff --git a/pkg/services/provisioning/datasources/datasources.go b/pkg/services/provisioning/datasources/datasources.go index 1fa0a3b3173..de6c876baad 100644 --- a/pkg/services/provisioning/datasources/datasources.go +++ b/pkg/services/provisioning/datasources/datasources.go @@ -11,7 +11,7 @@ import ( ) var ( - ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource can be marked as default") + ErrInvalidConfigToManyDefault = errors.New("datasource.yaml config is invalid. Only one datasource per organization can be marked as default") ) func Provision(configDirectory string) error { diff --git a/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml new file mode 100644 index 00000000000..f185abb6f53 --- /dev/null +++ b/pkg/services/provisioning/datasources/testdata/multiple-org-default/config.yaml @@ -0,0 +1,25 @@ +apiVersion: 1 + +datasources: + - orgId: 1 + name: prometheus + type: prometheus + isDefault: True + access: proxy + url: http://prometheus.example.com:9090 + - name: Graphite + type: graphite + access: proxy + url: http://localhost:8080 + - orgId: 2 + name: prometheus + type: prometheus + isDefault: True + access: proxy + url: http://prometheus.example.com:9090 + - orgId: 2 + name: Graphite + type: graphite + access: proxy + url: http://localhost:8080 + diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go index 8e06b5fed9d..87ccaf6b5d2 100644 --- a/pkg/services/rendering/phantomjs.go +++ b/pkg/services/rendering/phantomjs.go @@ -58,7 +58,9 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", opts.Encoding)}, cmdArgs...) } - commandCtx, _ := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + commandCtx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2) + defer cancel() + cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...) cmd.Stderr = cmd.Stdout diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 531a70b2101..af911dc22e6 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -73,6 +73,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error { alert.name, alert.state, alert.new_state_date, + alert.eval_data, alert.eval_date, alert.execution_error, dashboard.uid as dashboard_uid, diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index 79fa99864e7..d97deb45f0e 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -13,7 +13,7 @@ func mockTimeNow() { var timeSeed int64 timeNow = func() time.Time { fakeNow := time.Unix(timeSeed, 0) - timeSeed += 1 + timeSeed++ return fakeNow } } @@ -30,7 +30,7 @@ func TestAlertingDataAccess(t *testing.T) { InitTestDB(t) testDash := insertTestDashboard("dashboard with alerts", 1, 0, false, "alert") - + evalData, _ := simplejson.NewJson([]byte(`{"test": "test"}`)) items := []*m.Alert{ { PanelId: 1, @@ -40,6 +40,7 @@ func TestAlertingDataAccess(t *testing.T) { Message: "Alerting message", Settings: simplejson.New(), Frequency: 1, + EvalData: evalData, }, } @@ -104,8 +105,18 @@ func TestAlertingDataAccess(t *testing.T) { alert := alertQuery.Result[0] So(err2, ShouldBeNil) + So(alert.Id, ShouldBeGreaterThan, 0) + So(alert.DashboardId, ShouldEqual, testDash.Id) + So(alert.PanelId, ShouldEqual, 1) So(alert.Name, ShouldEqual, "Alerting title") So(alert.State, ShouldEqual, "pending") + So(alert.NewStateDate, ShouldNotBeNil) + So(alert.EvalData, ShouldNotBeNil) + So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test") + So(alert.EvalDate, ShouldNotBeNil) + So(alert.ExecutionError, ShouldEqual, "") + So(alert.DashboardUid, ShouldNotBeNil) + So(alert.DashboardSlug, ShouldEqual, "dashboard-with-alerts") }) Convey("Viewer cannot read alerts", func() { diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index e4aecf0391d..8ff78c4a0ff 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -181,7 +181,7 @@ func TestDashboardDataAccess(t *testing.T) { So(err, ShouldBeNil) So(query.Result.FolderId, ShouldEqual, 0) So(query.Result.CreatedBy, ShouldEqual, savedDash.CreatedBy) - So(query.Result.Created, ShouldEqual, savedDash.Created.Truncate(time.Second)) + So(query.Result.Created, ShouldHappenWithin, 3*time.Second, savedDash.Created) So(query.Result.UpdatedBy, ShouldEqual, 100) So(query.Result.Updated.IsZero(), ShouldBeFalse) }) @@ -387,6 +387,7 @@ func insertTestDashboardForPlugin(title string, orgId int64, folderId int64, isF func createUser(name string, role string, isAdmin bool) m.User { setting.AutoAssignOrg = true + setting.AutoAssignOrgId = 1 setting.AutoAssignOrgRole = role currentUserCmd := m.CreateUserCommand{Login: name, Email: name + "@test.com", Name: "a " + name, IsAdmin: isAdmin} diff --git a/pkg/services/sqlstore/migrations/user_mig.go b/pkg/services/sqlstore/migrations/user_mig.go index edcfbb7b889..400033aaa33 100644 --- a/pkg/services/sqlstore/migrations/user_mig.go +++ b/pkg/services/sqlstore/migrations/user_mig.go @@ -1,6 +1,12 @@ package migrations -import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +import ( + "fmt" + + "github.com/go-xorm/xorm" + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + "github.com/grafana/grafana/pkg/util" +) func addUserMigrations(mg *Migrator) { userV1 := Table{ @@ -107,4 +113,37 @@ func addUserMigrations(mg *Migrator) { mg.AddMigration("Add last_seen_at column to user", NewAddColumnMigration(userV2, &Column{ Name: "last_seen_at", Type: DB_DateTime, Nullable: true, })) + + // Adds salt & rands for old users who used ldap or oauth + mg.AddMigration("Add missing user data", &AddMissingUserSaltAndRandsMigration{}) +} + +type AddMissingUserSaltAndRandsMigration struct { + MigrationBase +} + +func (m *AddMissingUserSaltAndRandsMigration) Sql(dialect Dialect) string { + return "code migration" +} + +type TempUserDTO struct { + Id int64 + Login string +} + +func (m *AddMissingUserSaltAndRandsMigration) Exec(sess *xorm.Session, mg *Migrator) error { + users := make([]*TempUserDTO, 0) + + err := sess.Sql(fmt.Sprintf("SELECT id, login from %s WHERE rands = ''", mg.Dialect.Quote("user"))).Find(&users) + if err != nil { + return err + } + + for _, user := range users { + _, err := sess.Exec("UPDATE "+mg.Dialect.Quote("user")+" SET salt = ?, rands = ? WHERE id = ?", util.GetRandomString(10), util.GetRandomString(10), user.Id) + if err != nil { + return err + } + } + return nil } diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 9bdaaf7cc14..dead6f2b416 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -12,7 +12,7 @@ import ( type Migrator struct { x *xorm.Engine - dialect Dialect + Dialect Dialect migrations []Migration Logger log.Logger } @@ -31,7 +31,7 @@ func NewMigrator(engine *xorm.Engine) *Migrator { mg.x = engine mg.Logger = log.New("migrator") mg.migrations = make([]Migration, 0) - mg.dialect = NewDialect(mg.x) + mg.Dialect = NewDialect(mg.x) return mg } @@ -86,7 +86,7 @@ func (mg *Migrator) Start() error { continue } - sql := m.Sql(mg.dialect) + sql := m.Sql(mg.Dialect) record := MigrationLog{ MigrationId: m.Id(), @@ -122,7 +122,7 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { condition := m.GetCondition() if condition != nil { - sql, args := condition.Sql(mg.dialect) + sql, args := condition.Sql(mg.Dialect) results, err := sess.SQL(sql).Query(args...) if err != nil || len(results) == 0 { mg.Logger.Debug("Skipping migration condition not fulfilled", "id", m.Id()) @@ -130,7 +130,13 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { } } - _, err := sess.Exec(m.Sql(mg.dialect)) + var err error + if codeMigration, ok := m.(CodeMigration); ok { + err = codeMigration.Exec(sess, mg) + } else { + _, err = sess.Exec(m.Sql(mg.Dialect)) + } + if err != nil { mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err) return err diff --git a/pkg/services/sqlstore/migrator/types.go b/pkg/services/sqlstore/migrator/types.go index 26c46889daf..48354998d8d 100644 --- a/pkg/services/sqlstore/migrator/types.go +++ b/pkg/services/sqlstore/migrator/types.go @@ -3,6 +3,8 @@ package migrator import ( "fmt" "strings" + + "github.com/go-xorm/xorm" ) const ( @@ -19,6 +21,11 @@ type Migration interface { GetCondition() MigrationCondition } +type CodeMigration interface { + Migration + Exec(sess *xorm.Session, migrator *Migrator) error +} + type SQLType string type ColumnType string diff --git a/pkg/services/sqlstore/org_test.go b/pkg/services/sqlstore/org_test.go index 521a2a11c05..af8500707d5 100644 --- a/pkg/services/sqlstore/org_test.go +++ b/pkg/services/sqlstore/org_test.go @@ -17,6 +17,7 @@ func TestAccountDataAccess(t *testing.T) { Convey("Given single org mode", func() { setting.AutoAssignOrg = true + setting.AutoAssignOrgId = 1 setting.AutoAssignOrgRole = "Viewer" Convey("Users should be added to default organization", func() { diff --git a/pkg/services/sqlstore/team.go b/pkg/services/sqlstore/team.go index 9378ca37f60..72955df9a6a 100644 --- a/pkg/services/sqlstore/team.go +++ b/pkg/services/sqlstore/team.go @@ -22,6 +22,16 @@ func init() { bus.AddHandler("sql", GetTeamMembers) } +func getTeamSelectSqlBase() string { + return `SELECT + team.id as id, + team.org_id, + team.name as name, + team.email as email, + (SELECT COUNT(*) from team_member where team_member.team_id = team.id) as member_count + FROM team as team ` +} + func CreateTeam(cmd *m.CreateTeamCommand) error { return inTransaction(func(sess *DBSession) error { @@ -130,21 +140,15 @@ func isTeamNameTaken(orgId int64, name string, existingId int64, sess *DBSession func SearchTeams(query *m.SearchTeamsQuery) error { query.Result = m.SearchTeamQueryResult{ - Teams: make([]*m.SearchTeamDto, 0), + Teams: make([]*m.TeamDTO, 0), } queryWithWildcards := "%" + query.Query + "%" var sql bytes.Buffer params := make([]interface{}, 0) - sql.WriteString(`select - team.id as id, - team.org_id, - team.name as name, - team.email as email, - (select count(*) from team_member where team_member.team_id = team.id) as member_count - from team as team - where team.org_id = ?`) + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` WHERE team.org_id = ?`) params = append(params, query.OrgId) @@ -186,8 +190,14 @@ func SearchTeams(query *m.SearchTeamsQuery) error { } func GetTeamById(query *m.GetTeamByIdQuery) error { - var team m.Team - exists, err := x.Where("org_id=? and id=?", query.OrgId, query.Id).Get(&team) + var sql bytes.Buffer + + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` WHERE team.org_id = ? and team.id = ?`) + + var team m.TeamDTO + exists, err := x.Sql(sql.String(), query.OrgId, query.Id).Get(&team) + if err != nil { return err } @@ -202,13 +212,15 @@ func GetTeamById(query *m.GetTeamByIdQuery) error { // GetTeamsByUser is used by the Guardian when checking a users' permissions func GetTeamsByUser(query *m.GetTeamsByUserQuery) error { - query.Result = make([]*m.Team, 0) + query.Result = make([]*m.TeamDTO, 0) - sess := x.Table("team") - sess.Join("INNER", "team_member", "team.id=team_member.team_id") - sess.Where("team.org_id=? and team_member.user_id=?", query.OrgId, query.UserId) + var sql bytes.Buffer - err := sess.Find(&query.Result) + sql.WriteString(getTeamSelectSqlBase()) + sql.WriteString(` INNER JOIN team_member on team.id = team_member.team_id`) + sql.WriteString(` WHERE team.org_id = ? and team_member.user_id = ?`) + + err := x.Sql(sql.String(), query.OrgId, query.UserId).Find(&query.Result) return err } diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 5e9a085b26d..5d1b827e79f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -42,16 +42,23 @@ func getOrgIdForNewUser(cmd *m.CreateUserCommand, sess *DBSession) (int64, error var org m.Org if setting.AutoAssignOrg { - // right now auto assign to org with id 1 - has, err := sess.Where("id=?", 1).Get(&org) + has, err := sess.Where("id=?", setting.AutoAssignOrgId).Get(&org) if err != nil { return 0, err } if has { return org.Id, nil + } else { + if setting.AutoAssignOrgId == 1 { + org.Name = "Main Org." + org.Id = int64(setting.AutoAssignOrgId) + } else { + sqlog.Info("Could not create user: organization id %v does not exist", + setting.AutoAssignOrgId) + return 0, fmt.Errorf("Could not create user: organization id %v does not exist", + setting.AutoAssignOrgId) + } } - org.Name = "Main Org." - org.Id = 1 } else { org.Name = cmd.OrgName if len(org.Name) == 0 { @@ -106,9 +113,10 @@ func CreateUser(ctx context.Context, cmd *m.CreateUserCommand) error { LastSeenAt: time.Now().AddDate(-10, 0, 0), } + user.Salt = util.GetRandomString(10) + user.Rands = util.GetRandomString(10) + if len(cmd.Password) > 0 { - user.Salt = util.GetRandomString(10) - user.Rands = util.GetRandomString(10) user.Password = util.EncodePassword(cmd.Password, user.Salt) } diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index a76ae860b7d..b26dd235772 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/pkg/services/sqlstore/user_test.go @@ -15,6 +15,28 @@ func TestUserDataAccess(t *testing.T) { Convey("Testing DB", t, func() { InitTestDB(t) + Convey("Creating a user", func() { + cmd := &m.CreateUserCommand{ + Email: "usertest@test.com", + Name: "user name", + Login: "user_test_login", + } + + err := CreateUser(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("Loading a user", func() { + query := m.GetUserByIdQuery{Id: cmd.Result.Id} + err := GetUserById(&query) + So(err, ShouldBeNil) + + So(query.Result.Email, ShouldEqual, "usertest@test.com") + So(query.Result.Password, ShouldEqual, "") + So(query.Result.Rands, ShouldHaveLength, 10) + So(query.Result.Salt, ShouldHaveLength, 10) + }) + }) + Convey("Given 5 users", func() { var err error var cmd *m.CreateUserCommand diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index d8c8e6431c0..eb61568261d 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -100,6 +100,7 @@ var ( AllowUserSignUp bool AllowUserOrgCreate bool AutoAssignOrg bool + AutoAssignOrgId int AutoAssignOrgRole string VerifyEmailEnabled bool LoginHint string @@ -592,6 +593,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) + AutoAssignOrgId = users.Key("auto_assign_org_id").MustInt(1) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) LoginHint = users.Key("login_hint").String() diff --git a/pkg/social/gitlab_oauth.go b/pkg/social/gitlab_oauth.go new file mode 100644 index 00000000000..21463dabf8f --- /dev/null +++ b/pkg/social/gitlab_oauth.go @@ -0,0 +1,132 @@ +package social + +import ( + "encoding/json" + "fmt" + "net/http" + "regexp" + + "github.com/grafana/grafana/pkg/models" + + "golang.org/x/oauth2" +) + +type SocialGitlab struct { + *SocialBase + allowedDomains []string + allowedGroups []string + apiUrl string + allowSignup bool +} + +var ( + ErrMissingGroupMembership = &Error{"User not a member of one of the required groups"} +) + +func (s *SocialGitlab) Type() int { + return int(models.GITLAB) +} + +func (s *SocialGitlab) IsEmailAllowed(email string) bool { + return isEmailAllowed(email, s.allowedDomains) +} + +func (s *SocialGitlab) IsSignupAllowed() bool { + return s.allowSignup +} + +func (s *SocialGitlab) IsGroupMember(client *http.Client) bool { + if len(s.allowedGroups) == 0 { + return true + } + + for groups, url := s.GetGroups(client, s.apiUrl+"/groups"); groups != nil; groups, url = s.GetGroups(client, url) { + for _, allowedGroup := range s.allowedGroups { + for _, group := range groups { + if group == allowedGroup { + return true + } + } + } + } + + return false +} + +func (s *SocialGitlab) GetGroups(client *http.Client, url string) ([]string, string) { + type Group struct { + FullPath string `json:"full_path"` + } + + var ( + groups []Group + next string + ) + + if url == "" { + return nil, next + } + + response, err := HttpGet(client, url) + if err != nil { + s.log.Error("Error getting groups from GitLab API", "err", err) + return nil, next + } + + if err := json.Unmarshal(response.Body, &groups); err != nil { + s.log.Error("Error parsing JSON from GitLab API", "err", err) + return nil, next + } + + fullPaths := make([]string, len(groups)) + for i, group := range groups { + fullPaths[i] = group.FullPath + } + + if link, ok := response.Headers["Link"]; ok { + pattern := regexp.MustCompile(`<([^>]+)>; rel="next"`) + if matches := pattern.FindStringSubmatch(link[0]); matches != nil { + next = matches[1] + } + } + + return fullPaths, next +} + +func (s *SocialGitlab) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { + + var data struct { + Id int + Username string + Email string + Name string + State string + } + + response, err := HttpGet(client, s.apiUrl+"/user") + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + err = json.Unmarshal(response.Body, &data) + if err != nil { + return nil, fmt.Errorf("Error getting user info: %s", err) + } + + if data.State != "active" { + return nil, fmt.Errorf("User %s is inactive", data.Username) + } + + userInfo := &BasicUserInfo{ + Id: fmt.Sprintf("%d", data.Id), + Name: data.Name, + Login: data.Username, + Email: data.Email, + } + + if !s.IsGroupMember(client) { + return nil, ErrMissingGroupMembership + } + + return userInfo, nil +} diff --git a/pkg/social/social.go b/pkg/social/social.go index adbe5a912d9..2be71514629 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -55,7 +55,7 @@ func NewOAuthService() { setting.OAuthService = &setting.OAuther{} setting.OAuthService.OAuthInfos = make(map[string]*setting.OAuthInfo) - allOauthes := []string{"github", "google", "generic_oauth", "grafananet", "grafana_com"} + allOauthes := []string{"github", "gitlab", "google", "generic_oauth", "grafananet", "grafana_com"} for _, name := range allOauthes { sec := setting.Raw.Section("auth." + name) @@ -115,6 +115,20 @@ func NewOAuthService() { } } + // GitLab. + if name == "gitlab" { + SocialMap["gitlab"] = &SocialGitlab{ + SocialBase: &SocialBase{ + Config: &config, + log: logger, + }, + allowedDomains: info.AllowedDomains, + apiUrl: info.ApiUrl, + allowSignup: info.AllowSignup, + allowedGroups: util.SplitString(sec.Key("allowed_groups").String()), + } + } + // Google. if name == "google" { SocialMap["google"] = &SocialGoogle{ diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 8af97575ae9..92352a51315 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -14,8 +14,10 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" + "golang.org/x/sync/errgroup" "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/ec2/ec2iface" @@ -88,48 +90,80 @@ func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryCo Results: make(map[string]*tsdb.QueryResult), } - errCh := make(chan error, 1) - resCh := make(chan *tsdb.QueryResult, 1) + eg, ectx := errgroup.WithContext(ctx) - currentlyExecuting := 0 + getMetricDataQueries := make(map[string]map[string]*CloudWatchQuery) for i, model := range queryContext.Queries { queryType := model.Model.Get("type").MustString() if queryType != "timeSeriesQuery" && queryType != "" { continue } - currentlyExecuting++ - go func(refId string, index int) { - queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext) - currentlyExecuting-- - if err != nil { - errCh <- err - } else { - queryRes.RefId = refId - resCh <- queryRes + + RefId := queryContext.Queries[i].RefId + query, err := parseQuery(queryContext.Queries[i].Model) + if err != nil { + result.Results[RefId] = &tsdb.QueryResult{ + Error: err, } - }(model.RefId, i) + return result, nil + } + query.RefId = RefId + + if query.Id != "" { + if _, ok := getMetricDataQueries[query.Region]; !ok { + getMetricDataQueries[query.Region] = make(map[string]*CloudWatchQuery) + } + getMetricDataQueries[query.Region][query.Id] = query + continue + } + + if query.Id == "" && query.Expression != "" { + result.Results[query.RefId] = &tsdb.QueryResult{ + Error: fmt.Errorf("Invalid query: id should be set if using expression"), + } + return result, nil + } + + eg.Go(func() error { + queryRes, err := e.executeQuery(ectx, query, queryContext) + if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" { + return err + } + result.Results[queryRes.RefId] = queryRes + if err != nil { + result.Results[queryRes.RefId].Error = err + } + return nil + }) } - for currentlyExecuting != 0 { - select { - case res := <-resCh: - result.Results[res.RefId] = res - case err := <-errCh: - return result, err - case <-ctx.Done(): - return result, ctx.Err() + if len(getMetricDataQueries) > 0 { + for region, getMetricDataQuery := range getMetricDataQueries { + q := getMetricDataQuery + eg.Go(func() error { + queryResponses, err := e.executeGetMetricDataQuery(ectx, region, q, queryContext) + if ae, ok := err.(awserr.Error); ok && ae.Code() == "500" { + return err + } + for _, queryRes := range queryResponses { + result.Results[queryRes.RefId] = queryRes + if err != nil { + result.Results[queryRes.RefId].Error = err + } + } + return nil + }) } } + if err := eg.Wait(); err != nil { + return nil, err + } + return result, nil } -func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) { - query, err := parseQuery(parameters) - if err != nil { - return nil, err - } - +func (e *CloudWatchExecutor) executeQuery(ctx context.Context, query *CloudWatchQuery, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) { client, err := e.getClient(query.Region) if err != nil { return nil, err @@ -201,6 +235,139 @@ func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simpl return queryRes, nil } +func (e *CloudWatchExecutor) executeGetMetricDataQuery(ctx context.Context, region string, queries map[string]*CloudWatchQuery, queryContext *tsdb.TsdbQuery) ([]*tsdb.QueryResult, error) { + queryResponses := make([]*tsdb.QueryResult, 0) + + // validate query + for _, query := range queries { + if !(len(query.Statistics) == 1 && len(query.ExtendedStatistics) == 0) && + !(len(query.Statistics) == 0 && len(query.ExtendedStatistics) == 1) { + return queryResponses, errors.New("Statistics count should be 1") + } + } + + client, err := e.getClient(region) + if err != nil { + return queryResponses, err + } + + startTime, err := queryContext.TimeRange.ParseFrom() + if err != nil { + return queryResponses, err + } + + endTime, err := queryContext.TimeRange.ParseTo() + if err != nil { + return queryResponses, err + } + + params := &cloudwatch.GetMetricDataInput{ + StartTime: aws.Time(startTime), + EndTime: aws.Time(endTime), + ScanBy: aws.String("TimestampAscending"), + } + for _, query := range queries { + // 1 minutes resolutin metrics is stored for 15 days, 15 * 24 * 60 = 21600 + if query.HighResolution && (((endTime.Unix() - startTime.Unix()) / int64(query.Period)) > 21600) { + return nil, errors.New("too long query period") + } + + mdq := &cloudwatch.MetricDataQuery{ + Id: aws.String(query.Id), + ReturnData: aws.Bool(query.ReturnData), + } + if query.Expression != "" { + mdq.Expression = aws.String(query.Expression) + } else { + mdq.MetricStat = &cloudwatch.MetricStat{ + Metric: &cloudwatch.Metric{ + Namespace: aws.String(query.Namespace), + MetricName: aws.String(query.MetricName), + }, + Period: aws.Int64(int64(query.Period)), + } + for _, d := range query.Dimensions { + mdq.MetricStat.Metric.Dimensions = append(mdq.MetricStat.Metric.Dimensions, + &cloudwatch.Dimension{ + Name: d.Name, + Value: d.Value, + }) + } + if len(query.Statistics) == 1 { + mdq.MetricStat.Stat = query.Statistics[0] + } else { + mdq.MetricStat.Stat = query.ExtendedStatistics[0] + } + } + params.MetricDataQueries = append(params.MetricDataQueries, mdq) + } + + nextToken := "" + mdr := make(map[string]*cloudwatch.MetricDataResult) + for { + if nextToken != "" { + params.NextToken = aws.String(nextToken) + } + resp, err := client.GetMetricDataWithContext(ctx, params) + if err != nil { + return queryResponses, err + } + metrics.M_Aws_CloudWatch_GetMetricData.Add(float64(len(params.MetricDataQueries))) + + for _, r := range resp.MetricDataResults { + if _, ok := mdr[*r.Id]; !ok { + mdr[*r.Id] = r + } else { + mdr[*r.Id].Timestamps = append(mdr[*r.Id].Timestamps, r.Timestamps...) + mdr[*r.Id].Values = append(mdr[*r.Id].Values, r.Values...) + } + } + + if resp.NextToken == nil || *resp.NextToken == "" { + break + } + nextToken = *resp.NextToken + } + + for i, r := range mdr { + if *r.StatusCode != "Complete" { + return queryResponses, fmt.Errorf("Part of query is failed: %s", *r.StatusCode) + } + + queryRes := tsdb.NewQueryResult() + queryRes.RefId = queries[i].RefId + query := queries[*r.Id] + + series := tsdb.TimeSeries{ + Tags: map[string]string{}, + Points: make([]tsdb.TimePoint, 0), + } + for _, d := range query.Dimensions { + series.Tags[*d.Name] = *d.Value + } + s := "" + if len(query.Statistics) == 1 { + s = *query.Statistics[0] + } else { + s = *query.ExtendedStatistics[0] + } + series.Name = formatAlias(query, s, series.Tags) + + for j, t := range r.Timestamps { + expectedTimestamp := r.Timestamps[j].Add(time.Duration(query.Period) * time.Second) + if j > 0 && expectedTimestamp.Before(*t) { + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(expectedTimestamp.Unix()*1000))) + } + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(*r.Values[j]), float64((*t).Unix())*1000)) + } + + queryRes.Series = append(queryRes.Series, &series) + queryResponses = append(queryResponses, queryRes) + } + + return queryResponses, nil +} + func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) { var result []*cloudwatch.Dimension @@ -257,6 +424,9 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { return nil, err } + id := model.Get("id").MustString("") + expression := model.Get("expression").MustString("") + dimensions, err := parseDimensions(model) if err != nil { return nil, err @@ -295,6 +465,7 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { alias = "{{metric}}_{{stat}}" } + returnData := model.Get("returnData").MustBool(false) highResolution := model.Get("highResolution").MustBool(false) return &CloudWatchQuery{ @@ -306,11 +477,18 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { ExtendedStatistics: aws.StringSlice(extendedStatistics), Period: period, Alias: alias, + Id: id, + Expression: expression, + ReturnData: returnData, HighResolution: highResolution, }, nil } func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string { + if len(query.Id) > 0 && len(query.Expression) > 0 { + return query.Id + } + data := map[string]string{} data["region"] = query.Region data["namespace"] = query.Namespace @@ -338,6 +516,7 @@ func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]stri func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) { queryRes := tsdb.NewQueryResult() + queryRes.RefId = query.RefId var value float64 for _, s := range append(query.Statistics, query.ExtendedStatistics...) { series := tsdb.TimeSeries{ diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 136ee241c2e..ef1b53eaf1b 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -46,6 +46,7 @@ func init() { "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, + "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, "AWS/DynamoDB": {"ConditionalCheckFailedRequests", "ConsumedReadCapacityUnits", "ConsumedWriteCapacityUnits", "OnlineIndexConsumedWriteCapacity", "OnlineIndexPercentageProgress", "OnlineIndexThrottleEvents", "ProvisionedReadCapacityUnits", "ProvisionedWriteCapacityUnits", "ReadThrottleEvents", "ReturnedBytes", "ReturnedItemCount", "ReturnedRecordsCount", "SuccessfulRequestLatency", "SystemErrors", "TimeToLiveDeletedItemCount", "ThrottledRequests", "UserErrors", "WriteThrottleEvents"}, "AWS/EBS": {"VolumeReadBytes", "VolumeWriteBytes", "VolumeReadOps", "VolumeWriteOps", "VolumeTotalReadTime", "VolumeTotalWriteTime", "VolumeIdleTime", "VolumeQueueLength", "VolumeThroughputPercentage", "VolumeConsumedReadWriteOps", "BurstBalance"}, "AWS/EC2": {"CPUCreditUsage", "CPUCreditBalance", "CPUUtilization", "DiskReadOps", "DiskWriteOps", "DiskReadBytes", "DiskWriteBytes", "NetworkIn", "NetworkOut", "NetworkPacketsIn", "NetworkPacketsOut", "StatusCheckFailed", "StatusCheckFailed_Instance", "StatusCheckFailed_System"}, @@ -86,13 +87,14 @@ func init() { "AWS/Kinesis": {"GetRecords.Bytes", "GetRecords.IteratorAge", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Records", "GetRecords.Success", "IncomingBytes", "IncomingRecords", "PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "ReadProvisionedThroughputExceeded", "WriteProvisionedThroughputExceeded", "IteratorAgeMilliseconds", "OutgoingBytes", "OutgoingRecords"}, "AWS/KinesisAnalytics": {"Bytes", "MillisBehindLatest", "Records", "Success"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles", "IteratorAge"}, + "AWS/AppSync": {"Latency", "4XXError", "5XXError"}, "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, "AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, - "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, - "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, + "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"}, + "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"}, "AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"}, "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"}, @@ -118,6 +120,7 @@ func init() { "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, + "AWS/DX": {"ConnectionId"}, "AWS/DynamoDB": {"TableName", "GlobalSecondaryIndexName", "Operation", "StreamLabel"}, "AWS/EBS": {"VolumeId"}, "AWS/EC2": {"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, @@ -135,12 +138,13 @@ func init() { "AWS/Kinesis": {"StreamName", "ShardId"}, "AWS/KinesisAnalytics": {"Flow", "Id", "Application"}, "AWS/Lambda": {"FunctionName", "Resource", "Version", "Alias"}, + "AWS/AppSync": {"GraphQLAPIId"}, "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/NATGateway": {"NatGatewayId"}, "AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, - "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, + "AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"}, "AWS/RDS": {"DBInstanceIdentifier", "DBClusterIdentifier", "DbClusterIdentifier", "DatabaseClass", "EngineName", "Role"}, "AWS/Route53": {"HealthCheckId", "Region"}, "AWS/S3": {"BucketName", "StorageType", "FilterId"}, diff --git a/pkg/tsdb/cloudwatch/types.go b/pkg/tsdb/cloudwatch/types.go index 0737b64686d..1225fb9b31b 100644 --- a/pkg/tsdb/cloudwatch/types.go +++ b/pkg/tsdb/cloudwatch/types.go @@ -5,6 +5,7 @@ import ( ) type CloudWatchQuery struct { + RefId string Region string Namespace string MetricName string @@ -13,5 +14,8 @@ type CloudWatchQuery struct { ExtendedStatistics []*string Period int Alias string + Id string + Expression string + ReturnData bool HighResolution bool } diff --git a/pkg/tsdb/elasticsearch/client/client.go b/pkg/tsdb/elasticsearch/client/client.go index efc3ed1bda2..dff626a79eb 100644 --- a/pkg/tsdb/elasticsearch/client/client.go +++ b/pkg/tsdb/elasticsearch/client/client.go @@ -218,7 +218,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch elapsed := time.Now().Sub(start) clientLog.Debug("Decoded multisearch json response", "took", elapsed) - msr.status = res.StatusCode + msr.Status = res.StatusCode return &msr, nil } diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go index 8391e902ea4..952b5c4f806 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern.go @@ -248,13 +248,28 @@ var datePatternReplacements = map[string]string{ func formatDate(t time.Time, pattern string) string { var datePattern string - parts := strings.Split(strings.TrimLeft(pattern, "["), "]") - base := parts[0] - if len(parts) == 2 { - datePattern = parts[1] - } else { - datePattern = base - base = "" + base := "" + ltr := false + + if strings.HasPrefix(pattern, "[") { + parts := strings.Split(strings.TrimLeft(pattern, "["), "]") + base = parts[0] + if len(parts) == 2 { + datePattern = parts[1] + } else { + datePattern = base + base = "" + } + ltr = true + } else if strings.HasSuffix(pattern, "]") { + parts := strings.Split(strings.TrimRight(pattern, "]"), "[") + datePattern = parts[0] + if len(parts) == 2 { + base = parts[1] + } else { + base = "" + } + ltr = false } formatted := t.Format(patternToLayout(datePattern)) @@ -293,7 +308,11 @@ func formatDate(t time.Time, pattern string) string { formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", t.Hour()), -1) } - return base + formatted + if ltr { + return base + formatted + } + + return formatted + base } func patternToLayout(pattern string) string { diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go index 3bd823d8c87..ca20b39d532 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go @@ -28,29 +28,54 @@ func TestIndexPattern(t *testing.T) { to := fmt.Sprintf("%d", time.Date(2018, 5, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond)) indexPatternScenario(intervalHourly, "[data-]YYYY.MM.DD.HH", tsdb.NewTimeRange(from, to), func(indices []string) { - //So(indices, ShouldHaveLength, 1) + So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05.15.17") }) + indexPatternScenario(intervalHourly, "YYYY.MM.DD.HH[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05.15.17-data") + }) + indexPatternScenario(intervalDaily, "[data-]YYYY.MM.DD", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05.15") }) + indexPatternScenario(intervalDaily, "YYYY.MM.DD[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05.15-data") + }) + indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.20") }) + indexPatternScenario(intervalWeekly, "GGGG.WW[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.20-data") + }) + indexPatternScenario(intervalMonthly, "[data-]YYYY.MM", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018.05") }) + indexPatternScenario(intervalMonthly, "YYYY.MM[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018.05-data") + }) + indexPatternScenario(intervalYearly, "[data-]YYYY", tsdb.NewTimeRange(from, to), func(indices []string) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "data-2018") }) + + indexPatternScenario(intervalYearly, "YYYY[-data]", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "2018-data") + }) }) Convey("Hourly interval", t, func() { diff --git a/pkg/tsdb/elasticsearch/client/models.go b/pkg/tsdb/elasticsearch/client/models.go index a5810a9b109..a0d257d01a6 100644 --- a/pkg/tsdb/elasticsearch/client/models.go +++ b/pkg/tsdb/elasticsearch/client/models.go @@ -74,7 +74,7 @@ type MultiSearchRequest struct { // MultiSearchResponse represents a multi search response type MultiSearchResponse struct { - status int `json:"status,omitempty"` + Status int `json:"status,omitempty"` Responses []*SearchResponse `json:"responses"` } diff --git a/pkg/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index ad3d1edd5d7..caba043e7b6 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -6,26 +6,24 @@ import ( "strings" "time" - "strconv" - "github.com/grafana/grafana/pkg/tsdb" ) const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type MsSqlMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type msSqlMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query } -func NewMssqlMacroEngine() tsdb.SqlMacroEngine { - return &MsSqlMacroEngine{} +func newMssqlMacroEngine() tsdb.SqlMacroEngine { + return &msSqlMacroEngine{} } -func (m *MsSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *msSqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -66,7 +64,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": if len(args) == 0 { @@ -83,11 +81,11 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) @@ -97,28 +95,48 @@ func (m *MsSqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) - } else { - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.Query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } return fmt.Sprintf("FLOOR(DATEDIFF(second, '1970-01-01', %s)/%.0f)*%.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS [time]", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("FLOOR(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS [time]", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index 49368fe3631..8e0973b750c 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -14,7 +14,7 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := &MsSqlMacroEngine{} + engine := &msSqlMacroEngine{} query := &tsdb.Query{ Model: simplejson.New(), } @@ -55,27 +55,46 @@ func TestMacroEngine(t *testing.T) { Convey("interpolate __timeGroup function", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY FLOOR(DATEDIFF(second, '1970-01-01', time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") }) Convey("interpolate __timeGroup function with fill (value = NULL)", func() { _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', NULL)") fill := query.Model.Get("fill").MustBool() - fillNull := query.Model.Get("fillNull").MustBool() + fillMode := query.Model.Get("fillMode").MustString() fillInterval := query.Model.Get("fillInterval").MustInt() So(err, ShouldBeNil) So(fill, ShouldBeTrue) - So(fillNull, ShouldBeTrue) + So(fillMode, ShouldEqual, "null") + So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) + }) + + Convey("interpolate __timeGroup function with fill (value = previous)", func() { + _, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m', previous)") + + fill := query.Model.Get("fill").MustBool() + fillMode := query.Model.Get("fillMode").MustString() + fillInterval := query.Model.Get("fillInterval").MustInt() + + So(err, ShouldBeNil) + So(fill, ShouldBeTrue) + So(fillMode, ShouldEqual, "previous") So(fillInterval, ShouldEqual, 5*time.Minute.Seconds()) }) @@ -126,6 +145,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT FLOOR(time_column/300)*300") + So(sql2, ShouldEqual, sql+" AS [time]") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index eb71259b46b..72e57d03fa0 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -1,49 +1,40 @@ package mssql import ( - "container/list" - "context" "database/sql" "fmt" "strconv" "strings" - "math" - _ "github.com/denisenkom/go-mssqldb" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type MssqlQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("mssql", NewMssqlQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("mssql", newMssqlQueryEndpoint) } -func NewMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &MssqlQueryEndpoint{ - log: log.New("tsdb.mssql"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewMssqlMacroEngine(), - } +func newMssqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.mssql") cnnstr := generateConnectionString(datasource) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("mssql", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "mssql", + ConnectionString: cnnstr, + Datasource: datasource, + MetricColumnTypes: []string{"VARCHAR", "CHAR", "NVARCHAR", "NCHAR"}, } - return endpoint, nil + rowTransformer := mssqlRowTransformer{ + log: logger, + } + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMssqlMacroEngine(), logger) } func generateConnectionString(datasource *models.DataSource) string { @@ -70,71 +61,16 @@ func generateConnectionString(datasource *models.DataSource) string { ) } -// Query is the main function for the MssqlQueryEndpoint -func (e *MssqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type mssqlRowTransformer struct { + log log.Logger } -func (e MssqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - columnCount := len(columnNames) +func (t *mssqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) + valuePtrs := make([]interface{}, len(columnTypes)) - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, columnCount), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - - // check if there is a column named time - switch name { - case "time": - timeIndex = i - } - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("MsSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(columnTypes, rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds - // to make native mssql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { - values := make([]interface{}, len(types)) - valuePtrs := make([]interface{}, len(types)) - - for i, stype := range types { - e.log.Debug("type", "type", stype) + for i, stype := range columnTypes { + t.log.Debug("type", "type", stype) valuePtrs[i] = &values[i] } @@ -144,17 +80,17 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core. // convert types not handled by denisenkom/go-mssqldb // unhandled types are returned as []byte - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { if value, ok := values[i].([]byte); ok { - switch types[i].DatabaseTypeName() { + switch columnTypes[i].DatabaseTypeName() { case "MONEY", "SMALLMONEY", "DECIMAL": if v, err := strconv.ParseFloat(string(value), 64); err == nil { values[i] = v } else { - e.log.Debug("Rows", "Error converting numeric to float", value) + t.log.Debug("Rows", "Error converting numeric to float", value) } default: - e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value) + t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value) values[i] = string(value) } } @@ -162,157 +98,3 @@ func (e MssqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core. return values, nil } - -func (e MssqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "VARCHAR", "CHAR", "NVARCHAR", "NCHAR": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("MSSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(columnTypes, rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type CHAR, VARCHAR, NCHAR or NVARCHAR. metric column name: %s type: %s but datatype is %T", columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/mssql/mssql_test.go b/pkg/tsdb/mssql/mssql_test.go index db04d6d1f02..30d1da3bda1 100644 --- a/pkg/tsdb/mssql/mssql_test.go +++ b/pkg/tsdb/mssql/mssql_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" . "github.com/smartystreets/goconvey/convey" @@ -19,8 +20,9 @@ import ( // The tests require a MSSQL db named grafanatest and a user/password grafana/Password! // Use the docker/blocks/mssql_tests/docker-compose.yaml to spin up a // preconfigured MSSQL server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. // If needed, change the variable below to the IP address of the database. var serverIP = "localhost" @@ -28,19 +30,25 @@ func TestMSSQL(t *testing.T) { SkipConvey("MSSQL", t, func() { x := InitMSSQLTestDB(t) - endpoint := &MssqlQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewMssqlMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.mssql"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newMssqlQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { sql := ` IF OBJECT_ID('dbo.[mssql_types]', 'U') IS NOT NULL @@ -602,6 +610,31 @@ func TestMSSQL(t *testing.T) { So(queryResult.Series[1].Name, ShouldEqual, "valueTwo") }) + Convey("When doing a metric query with metric column and multiple value columns", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeEpoch(time), measurement, valueOne, valueTwo FROM metric_values ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") + }) + Convey("Given a stored procedure that takes @from and @to in epoch time", func() { sql := ` IF object_id('sp_test_epoch') IS NOT NULL @@ -627,21 +660,9 @@ func TestMSSQL(t *testing.T) { SELECT CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time BETWEEN DATEADD(s, @from, '1970-01-01') AND DATEADD(s, @to, '1970-01-01') AND - (@metric = 'ALL' OR measurement = @metric) - GROUP BY - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, - measurement - UNION ALL - SELECT - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value + measurement as metric, + avg(valueOne) as valueOne, + avg(valueTwo) as valueTwo FROM metric_values WHERE @@ -684,10 +705,10 @@ func TestMSSQL(t *testing.T) { So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) - So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") - So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") - So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") - So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") }) }) @@ -716,21 +737,9 @@ func TestMSSQL(t *testing.T) { SELECT CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value one' as metric, - avg(valueOne) as value - FROM - metric_values - WHERE - time BETWEEN @from AND @to AND - (@metric = 'ALL' OR measurement = @metric) - GROUP BY - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval, - measurement - UNION ALL - SELECT - CAST(ROUND(DATEDIFF(second, '1970-01-01', time)/CAST(@dInterval as float), 0) as bigint)*@dInterval as time, - measurement + ' - value two' as metric, - avg(valueTwo) as value + measurement as metric, + avg(valueOne) as valueOne, + avg(valueTwo) as valueTwo FROM metric_values WHERE @@ -773,10 +782,10 @@ func TestMSSQL(t *testing.T) { So(queryResult.Error, ShouldBeNil) So(len(queryResult.Series), ShouldEqual, 4) - So(queryResult.Series[0].Name, ShouldEqual, "Metric A - value one") - So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") - So(queryResult.Series[2].Name, ShouldEqual, "Metric A - value two") - So(queryResult.Series[3].Name, ShouldEqual, "Metric B - value two") + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") }) }) }) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index 584f731f3b8..0dabdd7c283 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -3,7 +3,6 @@ package mysql import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -14,18 +13,18 @@ import ( const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type MySqlMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type mySqlMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query } -func NewMysqlMacroEngine() tsdb.SqlMacroEngine { - return &MySqlMacroEngine{} +func newMysqlMacroEngine() tsdb.SqlMacroEngine { + return &mySqlMacroEngine{} } -func (m *MySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *mySqlMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error @@ -66,7 +65,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__timeEpoch", "__time": if len(args) == 0 { @@ -78,11 +77,11 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval", name) @@ -92,28 +91,48 @@ func (m *MySqlMacroEngine) evaluateMacro(name string, args []string) (string, er return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) - } else { - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.Query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } return fmt.Sprintf("UNIX_TIMESTAMP(%s) DIV %.0f * %.0f", args[0], interval.Seconds(), interval.Seconds()), nil + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("%s DIV %v * %v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/mysql/macros_test.go b/pkg/tsdb/mysql/macros_test.go index 2561661b385..fe153ca3e2d 100644 --- a/pkg/tsdb/mysql/macros_test.go +++ b/pkg/tsdb/mysql/macros_test.go @@ -12,7 +12,7 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := &MySqlMacroEngine{} + engine := &mySqlMacroEngine{} query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { @@ -38,16 +38,22 @@ func TestMacroEngine(t *testing.T) { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces around arguments", func() { sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroupAlias(time_column , '5m')") + So(err, ShouldBeNil) So(sql, ShouldEqual, "GROUP BY UNIX_TIMESTAMP(time_column) DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeFilter function", func() { @@ -91,6 +97,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT time_column DIV 300 * 300") + So(sql2, ShouldEqual, sql+" AS \"time\"") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 7eceaffdb09..645f6b49bbb 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -1,39 +1,24 @@ package mysql import ( - "container/list" - "context" "database/sql" "fmt" - "math" "reflect" "strconv" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type MysqlQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("mysql", NewMysqlQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("mysql", newMysqlQueryEndpoint) } -func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &MysqlQueryEndpoint{ - log: log.New("tsdb.mysql"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewMysqlMacroEngine(), - } +func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.mysql") cnnstr := fmt.Sprintf("%s:%s@%s(%s)/%s?collation=utf8mb4_unicode_ci&parseTime=true&loc=UTC&allowNativePasswords=true", datasource.User, @@ -42,85 +27,35 @@ func NewMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Url, datasource.Database, ) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("mysql", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "mysql", + ConnectionString: cnnstr, + Datasource: datasource, + TimeColumnNames: []string{"time", "time_sec"}, + MetricColumnTypes: []string{"CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT"}, } - return endpoint, nil + rowTransformer := mysqlRowTransformer{ + log: logger, + } + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newMysqlMacroEngine(), logger) } -// Query is the main function for the MysqlExecutor -func (e *MysqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type mysqlRowTransformer struct { + log log.Logger } -func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - columnCount := len(columnNames) - - if err != nil { - return err - } - - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, columnCount), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - // check if there is a column named time - for i, col := range columnNames { - switch col { - case "time", "time_sec": - timeIndex = i - } - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - - values := make([]interface{}, len(types)) +func (t *mysqlRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) for i := range values { - scanType := types[i].ScanType() + scanType := columnTypes[i].ScanType() values[i] = reflect.New(scanType).Interface() - if types[i].DatabaseTypeName() == "BIT" { + if columnTypes[i].DatabaseTypeName() == "BIT" { values[i] = new([]byte) } } @@ -129,7 +64,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er return nil, err } - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { typeName := reflect.ValueOf(values[i]).Type().String() switch typeName { @@ -158,7 +93,7 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er } } - if types[i].DatabaseTypeName() == "DECIMAL" { + if columnTypes[i].DatabaseTypeName() == "DECIMAL" { f, err := strconv.ParseFloat(values[i].(string), 64) if err == nil { @@ -171,159 +106,3 @@ func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, er return values, nil } - -func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time", "time_sec": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "CHAR", "VARCHAR", "TINYTEXT", "TEXT", "MEDIUMTEXT", "LONGTEXT": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time or time_sec") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time/time_sec, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - rowCount++ - - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 850a37617e2..ca6df8e360e 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" @@ -21,8 +22,9 @@ import ( // The tests require a MySQL db named grafana_ds_tests and a user/password grafana/password // Use the docker/blocks/mysql_tests/docker-compose.yaml to spin up a // preconfigured MySQL server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. func TestMySQL(t *testing.T) { // change to true to run the MySQL tests runMySqlTests := false @@ -35,19 +37,25 @@ func TestMySQL(t *testing.T) { Convey("MySQL", t, func() { x := InitMySQLTestDB(t) - endpoint := &MysqlQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewMysqlMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.mysql"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newMysqlQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { if exists, err := sess.IsTableExist("mysql_types"); err != nil || exists { So(err, ShouldBeNil) @@ -287,7 +295,7 @@ func TestMySQL(t *testing.T) { }) - Convey("When doing a metric query using timeGroup with float fill enabled", func() { + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { @@ -312,6 +320,35 @@ func TestMySQL(t *testing.T) { points := queryResult.Series[0].Points So(points[3][0].Float64, ShouldEqual, 1.5) }) + + Convey("When doing a metric query using timeGroup with previous fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', previous) as time_sec, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[2][0].Float64, ShouldEqual, 15.0) + So(points[3][0].Float64, ShouldEqual, 15.0) + So(points[6][0].Float64, ShouldEqual, 20.0) + }) + }) Convey("Given a table with metrics having multiple values and measurements", func() { @@ -626,6 +663,31 @@ func TestMySQL(t *testing.T) { So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") }) + Convey("When doing a metric query with metric column and multiple value columns", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__time(time), measurement as metric, valueOne, valueTwo FROM metric_values ORDER BY 1,2`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") + }) + Convey("When doing a metric query grouping by time should return correct series", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 61e88418ff4..0a2ea1d2af6 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -3,7 +3,6 @@ package postgres import ( "fmt" "regexp" - "strconv" "strings" "time" @@ -14,22 +13,40 @@ import ( const rsIdentifier = `([_a-zA-Z0-9]+)` const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)` -type PostgresMacroEngine struct { - TimeRange *tsdb.TimeRange - Query *tsdb.Query +type postgresMacroEngine struct { + timeRange *tsdb.TimeRange + query *tsdb.Query + timescaledb bool } -func NewPostgresMacroEngine() tsdb.SqlMacroEngine { - return &PostgresMacroEngine{} +func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine { + return &postgresMacroEngine{timescaledb: timescaledb} } -func (m *PostgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { - m.TimeRange = timeRange - m.Query = query +func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) { + m.timeRange = timeRange + m.query = query rExp, _ := regexp.Compile(sExpr) var macroError error sql = replaceAllStringSubmatchFunc(rExp, sql, func(groups []string) string { + + // detect if $__timeGroup is supposed to add AS time for pre 5.3 compatibility + // if there is a ',' directly after the macro call $__timeGroup is probably used + // in the old way. Inside window function ORDER BY $__timeGroup will be followed + // by ')' + if groups[1] == "__timeGroup" { + if index := strings.Index(sql, groups[0]); index >= 0 { + index += len(groups[0]) + if len(sql) > index { + // check for character after macro expression + if sql[index] == ',' { + groups[1] = "__timeGroupAlias" + } + } + } + } + args := strings.Split(groups[2], ",") for i, arg := range args { args[i] = strings.Trim(arg, " ") @@ -66,7 +83,7 @@ func replaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]str return result + str[lastIndex:] } -func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { +func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, error) { switch name { case "__time": if len(args) == 0 { @@ -83,11 +100,11 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("%s BETWEEN '%s' AND '%s'", args[0], m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339), m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeFrom": - return fmt.Sprintf("'%s'", m.TimeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetFromAsTimeUTC().Format(time.RFC3339)), nil case "__timeTo": - return fmt.Sprintf("'%s'", m.TimeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil + return fmt.Sprintf("'%s'", m.timeRange.GetToAsTimeUTC().Format(time.RFC3339)), nil case "__timeGroup": if len(args) < 2 { return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) @@ -97,28 +114,53 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string, return "", fmt.Errorf("error parsing interval %v", args[1]) } if len(args) == 3 { - m.Query.Model.Set("fill", true) - m.Query.Model.Set("fillInterval", interval.Seconds()) - if args[2] == "NULL" { - m.Query.Model.Set("fillNull", true) - } else { - floatVal, err := strconv.ParseFloat(args[2], 64) - if err != nil { - return "", fmt.Errorf("error parsing fill value %v", args[2]) - } - m.Query.Model.Set("fillValue", floatVal) + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err } } - return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v AS time", args[0], interval.Seconds(), interval.Seconds()), nil + + if m.timescaledb { + return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil + } else { + return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + } + case "__timeGroupAlias": + tg, err := m.evaluateMacro("__timeGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err case "__unixEpochFilter": if len(args) == 0 { return "", fmt.Errorf("missing time column argument for macro %v", name) } - return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.TimeRange.GetFromAsSecondsEpoch(), args[0], m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%s >= %d AND %s <= %d", args[0], m.timeRange.GetFromAsSecondsEpoch(), args[0], m.timeRange.GetToAsSecondsEpoch()), nil case "__unixEpochFrom": - return fmt.Sprintf("%d", m.TimeRange.GetFromAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetFromAsSecondsEpoch()), nil case "__unixEpochTo": - return fmt.Sprintf("%d", m.TimeRange.GetToAsSecondsEpoch()), nil + return fmt.Sprintf("%d", m.timeRange.GetToAsSecondsEpoch()), nil + case "__unixEpochGroup": + if len(args) < 2 { + return "", fmt.Errorf("macro %v needs time column and interval and optional fill value", name) + } + interval, err := time.ParseDuration(strings.Trim(args[1], `'`)) + if err != nil { + return "", fmt.Errorf("error parsing interval %v", args[1]) + } + if len(args) == 3 { + err := tsdb.SetupFillmode(m.query, interval, args[2]) + if err != nil { + return "", err + } + } + return fmt.Sprintf("floor(%s/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil + case "__unixEpochGroupAlias": + tg, err := m.evaluateMacro("__unixEpochGroup", args) + if err == nil { + return tg + " AS \"time\"", err + } + return "", err default: return "", fmt.Errorf("Unknown macro %v", name) } diff --git a/pkg/tsdb/postgres/macros_test.go b/pkg/tsdb/postgres/macros_test.go index 8c581850430..b0b7a28ddd4 100644 --- a/pkg/tsdb/postgres/macros_test.go +++ b/pkg/tsdb/postgres/macros_test.go @@ -12,7 +12,10 @@ import ( func TestMacroEngine(t *testing.T) { Convey("MacroEngine", t, func() { - engine := NewPostgresMacroEngine() + timescaledbEnabled := false + engine := newPostgresMacroEngine(timescaledbEnabled) + timescaledbEnabled = true + engineTS := newPostgresMacroEngine(timescaledbEnabled) query := &tsdb.Query{} Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() { @@ -48,20 +51,55 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select '%s'", from.Format(time.RFC3339))) }) - Convey("interpolate __timeGroup function", func() { + Convey("interpolate __timeGroup function pre 5.3 compatibility", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m'), value") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 AS \"time\", value") + + sql, err = engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m') as time, value") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300 as time, value") + }) + + Convey("interpolate __timeGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__timeGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") }) Convey("interpolate __timeGroup function with spaces between args", func() { - sql, err := engine.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + sql, err := engine.Interpolate(query, timeRange, "$__timeGroup(time_column , '5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "$__timeGroupAlias(time_column , '5m')") So(err, ShouldBeNil) - So(sql, ShouldEqual, "GROUP BY floor(extract(epoch from time_column)/300)*300 AS time") + So(sql, ShouldEqual, "floor(extract(epoch from time_column)/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") + }) + + Convey("interpolate __timeGroup function with TimescaleDB enabled", func() { + + sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") + }) + + Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() { + + sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)") }) Convey("interpolate __timeTo function", func() { @@ -91,6 +129,18 @@ func TestMacroEngine(t *testing.T) { So(sql, ShouldEqual, fmt.Sprintf("select %d", to.Unix())) }) + + Convey("interpolate __unixEpochGroup function", func() { + + sql, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroup(time_column,'5m')") + So(err, ShouldBeNil) + sql2, err := engine.Interpolate(query, timeRange, "SELECT $__unixEpochGroupAlias(time_column,'5m')") + So(err, ShouldBeNil) + + So(sql, ShouldEqual, "SELECT floor(time_column/300)*300") + So(sql2, ShouldEqual, sql+" AS \"time\"") + }) + }) Convey("Given a time range between 1960-02-01 07:00 and 1965-02-03 08:00", func() { diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index fdf09216e51..4bcf06638f4 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -1,46 +1,40 @@ package postgres import ( - "container/list" - "context" - "fmt" - "math" + "database/sql" "net/url" "strconv" "github.com/go-xorm/core" - "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/tsdb" ) -type PostgresQueryEndpoint struct { - sqlEngine tsdb.SqlEngine - log log.Logger -} - func init() { - tsdb.RegisterTsdbQueryEndpoint("postgres", NewPostgresQueryEndpoint) + tsdb.RegisterTsdbQueryEndpoint("postgres", newPostgresQueryEndpoint) } -func NewPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { - endpoint := &PostgresQueryEndpoint{ - log: log.New("tsdb.postgres"), - } - - endpoint.sqlEngine = &tsdb.DefaultSqlEngine{ - MacroEngine: NewPostgresMacroEngine(), - } +func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoint, error) { + logger := log.New("tsdb.postgres") cnnstr := generateConnectionString(datasource) - endpoint.log.Debug("getEngine", "connection", cnnstr) + logger.Debug("getEngine", "connection", cnnstr) - if err := endpoint.sqlEngine.InitEngine("postgres", datasource, cnnstr); err != nil { - return nil, err + config := tsdb.SqlQueryEndpointConfiguration{ + DriverName: "postgres", + ConnectionString: cnnstr, + Datasource: datasource, + MetricColumnTypes: []string{"UNKNOWN", "TEXT", "VARCHAR", "CHAR"}, } - return endpoint, nil + rowTransformer := postgresRowTransformer{ + log: logger, + } + + timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false) + + return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(timescaledb), logger) } func generateConnectionString(datasource *models.DataSource) string { @@ -53,74 +47,25 @@ func generateConnectionString(datasource *models.DataSource) string { } sslmode := datasource.JsonData.Get("sslmode").MustString("verify-full") - u := &url.URL{Scheme: "postgres", User: url.UserPassword(datasource.User, password), Host: datasource.Url, Path: datasource.Database, RawQuery: "sslmode=" + sslmode} + u := &url.URL{ + Scheme: "postgres", + User: url.UserPassword(datasource.User, password), + Host: datasource.Url, Path: datasource.Database, + RawQuery: "sslmode=" + url.QueryEscape(sslmode), + } + return u.String() } -func (e *PostgresQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { - return e.sqlEngine.Query(ctx, dsInfo, tsdbQuery, e.transformToTimeSeries, e.transformToTable) +type postgresRowTransformer struct { + log log.Logger } -func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - columnNames, err := rows.Columns() - if err != nil { - return err - } +func (t *postgresRowTransformer) Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) { + values := make([]interface{}, len(columnTypes)) + valuePtrs := make([]interface{}, len(columnTypes)) - table := &tsdb.Table{ - Columns: make([]tsdb.TableColumn, len(columnNames)), - Rows: make([]tsdb.RowValues, 0), - } - - for i, name := range columnNames { - table.Columns[i].Text = name - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - - // check if there is a column named time - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - } - } - - for ; rows.Next(); rowCount++ { - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native postgres datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - table.Rows = append(table.Rows, values) - } - - result.Tables = append(result.Tables, table) - result.Meta.Set("rowCount", rowCount) - return nil -} - -func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) { - types, err := rows.ColumnTypes() - if err != nil { - return nil, err - } - - values := make([]interface{}, len(types)) - valuePtrs := make([]interface{}, len(types)) - - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { valuePtrs[i] = &values[i] } @@ -130,20 +75,20 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, // convert types not handled by lib/pq // unhandled types are returned as []byte - for i := 0; i < len(types); i++ { + for i := 0; i < len(columnTypes); i++ { if value, ok := values[i].([]byte); ok { - switch types[i].DatabaseTypeName() { + switch columnTypes[i].DatabaseTypeName() { case "NUMERIC": if v, err := strconv.ParseFloat(string(value), 64); err == nil { values[i] = v } else { - e.log.Debug("Rows", "Error converting numeric to float", value) + t.log.Debug("Rows", "Error converting numeric to float", value) } case "UNKNOWN", "CIDR", "INET", "MACADDR": // char literals have type UNKNOWN values[i] = string(value) default: - e.log.Debug("Rows", "Unknown database type", types[i].DatabaseTypeName(), "value", value) + t.log.Debug("Rows", "Unknown database type", columnTypes[i].DatabaseTypeName(), "value", value) values[i] = string(value) } } @@ -151,159 +96,3 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, return values, nil } - -func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult, tsdbQuery *tsdb.TsdbQuery) error { - pointsBySeries := make(map[string]*tsdb.TimeSeries) - seriesByQueryOrder := list.New() - - columnNames, err := rows.Columns() - if err != nil { - return err - } - - columnTypes, err := rows.ColumnTypes() - if err != nil { - return err - } - - rowLimit := 1000000 - rowCount := 0 - timeIndex := -1 - metricIndex := -1 - - // check columns of resultset: a column named time is mandatory - // the first text column is treated as metric name unless a column named metric is present - for i, col := range columnNames { - switch col { - case "time": - timeIndex = i - case "metric": - metricIndex = i - default: - if metricIndex == -1 { - switch columnTypes[i].DatabaseTypeName() { - case "UNKNOWN", "TEXT", "VARCHAR", "CHAR": - metricIndex = i - } - } - } - } - - if timeIndex == -1 { - return fmt.Errorf("Found no column named time") - } - - fillMissing := query.Model.Get("fill").MustBool(false) - var fillInterval float64 - fillValue := null.Float{} - if fillMissing { - fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { - fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() - fillValue.Valid = true - } - } - - for rows.Next() { - var timestamp float64 - var value null.Float - var metric string - - if rowCount > rowLimit { - return fmt.Errorf("PostgreSQL query row limit exceeded, limit %d", rowLimit) - } - - values, err := e.getTypedRowData(rows) - if err != nil { - return err - } - - // converts column named time to unix timestamp in milliseconds to make - // native mysql datetime types and epoch dates work in - // annotation and table queries. - tsdb.ConvertSqlTimeColumnToEpochMs(values, timeIndex) - - switch columnValue := values[timeIndex].(type) { - case int64: - timestamp = float64(columnValue) - case float64: - timestamp = columnValue - default: - return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) - } - - if metricIndex >= 0 { - if columnValue, ok := values[metricIndex].(string); ok { - metric = columnValue - } else { - return fmt.Errorf("Column metric must be of type char,varchar or text, got: %T %v", values[metricIndex], values[metricIndex]) - } - } - - for i, col := range columnNames { - if i == timeIndex || i == metricIndex { - continue - } - - if value, err = tsdb.ConvertSqlValueColumnToFloat(col, values[i]); err != nil { - return err - } - - if metricIndex == -1 { - metric = col - } - - series, exist := pointsBySeries[metric] - if !exist { - series = &tsdb.TimeSeries{Name: metric} - pointsBySeries[metric] = series - seriesByQueryOrder.PushBack(metric) - } - - if fillMissing { - var intervalStart float64 - if !exist { - intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) - } else { - intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval - } - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - - for i := intervalStart; i < timestamp; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - - series.Points = append(series.Points, tsdb.TimePoint{value, null.FloatFrom(timestamp)}) - - e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) - rowCount++ - - } - } - - for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { - key := elem.Value.(string) - result.Series = append(result.Series, pointsBySeries[key]) - - if fillMissing { - series := pointsBySeries[key] - // fill in values from last fetched value till interval end - intervalStart := series.Points[len(series.Points)-1][1].Float64 - intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) - - // align interval start - intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval - for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { - series.Points = append(series.Points, tsdb.TimePoint{fillValue, null.FloatFrom(i)}) - rowCount++ - } - } - } - - result.Meta.Set("rowCount", rowCount) - return nil -} diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index a3a6d6546df..4e05f676682 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -8,8 +8,9 @@ import ( "time" "github.com/go-xorm/xorm" + "github.com/grafana/grafana/pkg/components/securejsondata" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore/sqlutil" "github.com/grafana/grafana/pkg/tsdb" @@ -22,10 +23,11 @@ import ( // The tests require a PostgreSQL db named grafanadstest and a user/password grafanatest/grafanatest! // Use the docker/blocks/postgres_tests/docker-compose.yaml to spin up a // preconfigured Postgres server suitable for running these tests. -// There is also a dashboard.json in same directory that you can import to Grafana -// once you've created a datasource for the test server/database. +// There is also a datasource and dashboard provisioned by devenv scripts that you can +// use to verify that the generated data are vizualized as expected, see +// devenv/README.md for setup instructions. func TestPostgres(t *testing.T) { - // change to true to run the MySQL tests + // change to true to run the PostgreSQL tests runPostgresTests := false // runPostgresTests := true @@ -36,19 +38,25 @@ func TestPostgres(t *testing.T) { Convey("PostgreSQL", t, func() { x := InitPostgresTestDB(t) - endpoint := &PostgresQueryEndpoint{ - sqlEngine: &tsdb.DefaultSqlEngine{ - MacroEngine: NewPostgresMacroEngine(), - XormEngine: x, - }, - log: log.New("tsdb.postgres"), + origXormEngine := tsdb.NewXormEngine + tsdb.NewXormEngine = func(d, c string) (*xorm.Engine, error) { + return x, nil } - sess := x.NewSession() - defer sess.Close() + endpoint, err := newPostgresQueryEndpoint(&models.DataSource{ + JsonData: simplejson.New(), + SecureJsonData: securejsondata.SecureJsonData{}, + }) + So(err, ShouldBeNil) + sess := x.NewSession() fromStart := time.Date(2018, 3, 15, 13, 0, 0, 0, time.UTC).In(time.Local) + Reset(func() { + sess.Close() + tsdb.NewXormEngine = origXormEngine + }) + Convey("Given a table with different native data types", func() { sql := ` DROP TABLE IF EXISTS postgres_types; @@ -175,7 +183,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m'), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m') AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -219,7 +227,7 @@ func TestPostgres(t *testing.T) { Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', NULL), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', NULL) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -268,12 +276,12 @@ func TestPostgres(t *testing.T) { }) - Convey("When doing a metric query using timeGroup with float fill enabled", func() { + Convey("When doing a metric query using timeGroup with value fill enabled", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ { Model: simplejson.NewFromAny(map[string]interface{}{ - "rawSql": "SELECT $__timeGroup(time, '5m', 1.5), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "rawSql": "SELECT $__timeGroup(time, '5m', 1.5) AS time, avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", "format": "time_series", }), RefId: "A", @@ -295,6 +303,34 @@ func TestPostgres(t *testing.T) { }) }) + Convey("When doing a metric query using timeGroup with previous fill enabled", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": "SELECT $__timeGroup(time, '5m', previous), avg(value) as value FROM metric GROUP BY 1 ORDER BY 1", + "format": "time_series", + }), + RefId: "A", + }, + }, + TimeRange: &tsdb.TimeRange{ + From: fmt.Sprintf("%v", fromStart.Unix()*1000), + To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000), + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + points := queryResult.Series[0].Points + So(points[2][0].Float64, ShouldEqual, 15.0) + So(points[3][0].Float64, ShouldEqual, 15.0) + So(points[6][0].Float64, ShouldEqual, 20.0) + }) + Convey("Given a table with metrics having multiple values and measurements", func() { type metric_values struct { Time time.Time @@ -560,6 +596,31 @@ func TestPostgres(t *testing.T) { So(queryResult.Series[1].Name, ShouldEqual, "Metric B - value one") }) + Convey("When doing a metric query with metric column and multiple value columns", func() { + query := &tsdb.TsdbQuery{ + Queries: []*tsdb.Query{ + { + Model: simplejson.NewFromAny(map[string]interface{}{ + "rawSql": `SELECT $__timeEpoch(time), measurement as metric, "valueOne", "valueTwo" FROM metric_values ORDER BY 1`, + "format": "time_series", + }), + RefId: "A", + }, + }, + } + + resp, err := endpoint.Query(nil, nil, query) + So(err, ShouldBeNil) + queryResult := resp.Results["A"] + So(queryResult.Error, ShouldBeNil) + + So(len(queryResult.Series), ShouldEqual, 4) + So(queryResult.Series[0].Name, ShouldEqual, "Metric A valueOne") + So(queryResult.Series[1].Name, ShouldEqual, "Metric A valueTwo") + So(queryResult.Series[2].Name, ShouldEqual, "Metric B valueOne") + So(queryResult.Series[3].Name, ShouldEqual, "Metric B valueTwo") + }) + Convey("When doing a metric query grouping by time should return correct series", func() { query := &tsdb.TsdbQuery{ Queries: []*tsdb.Query{ diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 82a9b8f0d88..454853c7cc8 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -1,11 +1,18 @@ package tsdb import ( + "container/list" "context" + "database/sql" "fmt" + "math" + "strconv" + "strings" "sync" "time" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/components/null" "github.com/go-xorm/core" @@ -14,27 +21,15 @@ import ( "github.com/grafana/grafana/pkg/models" ) -// SqlEngine is a wrapper class around xorm for relational database data sources. -type SqlEngine interface { - InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error - Query( - ctx context.Context, - ds *models.DataSource, - query *TsdbQuery, - transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - ) (*Response, error) -} - // SqlMacroEngine interpolates macros into sql. It takes in the Query to have access to query context and // timeRange to be able to generate queries that use from and to. type SqlMacroEngine interface { Interpolate(query *Query, timeRange *TimeRange, sql string) (string, error) } -type DefaultSqlEngine struct { - MacroEngine SqlMacroEngine - XormEngine *xorm.Engine +// SqlTableRowTransformer transforms a query result row to RowValues with proper types. +type SqlTableRowTransformer interface { + Transform(columnTypes []*sql.ColumnType, rows *core.Rows) (RowValues, error) } type engineCacheType struct { @@ -48,68 +43,98 @@ var engineCache = engineCacheType{ versions: make(map[int64]int), } -// InitEngine creates the db connection and inits the xorm engine or loads it from the engine cache -func (e *DefaultSqlEngine) InitEngine(driverName string, dsInfo *models.DataSource, cnnstr string) error { +var NewXormEngine = func(driverName string, connectionString string) (*xorm.Engine, error) { + return xorm.NewEngine(driverName, connectionString) +} + +type sqlQueryEndpoint struct { + macroEngine SqlMacroEngine + rowTransformer SqlTableRowTransformer + engine *xorm.Engine + timeColumnNames []string + metricColumnTypes []string + log log.Logger +} + +type SqlQueryEndpointConfiguration struct { + DriverName string + Datasource *models.DataSource + ConnectionString string + TimeColumnNames []string + MetricColumnTypes []string +} + +var NewSqlQueryEndpoint = func(config *SqlQueryEndpointConfiguration, rowTransformer SqlTableRowTransformer, macroEngine SqlMacroEngine, log log.Logger) (TsdbQueryEndpoint, error) { + queryEndpoint := sqlQueryEndpoint{ + rowTransformer: rowTransformer, + macroEngine: macroEngine, + timeColumnNames: []string{"time"}, + log: log, + } + + if len(config.TimeColumnNames) > 0 { + queryEndpoint.timeColumnNames = config.TimeColumnNames + } + + if len(config.MetricColumnTypes) > 0 { + queryEndpoint.metricColumnTypes = config.MetricColumnTypes + } + engineCache.Lock() defer engineCache.Unlock() - if engine, present := engineCache.cache[dsInfo.Id]; present { - if version := engineCache.versions[dsInfo.Id]; version == dsInfo.Version { - e.XormEngine = engine - return nil + if engine, present := engineCache.cache[config.Datasource.Id]; present { + if version := engineCache.versions[config.Datasource.Id]; version == config.Datasource.Version { + queryEndpoint.engine = engine + return &queryEndpoint, nil } } - engine, err := xorm.NewEngine(driverName, cnnstr) + engine, err := NewXormEngine(config.DriverName, config.ConnectionString) if err != nil { - return err + return nil, err } engine.SetMaxOpenConns(10) engine.SetMaxIdleConns(10) - engineCache.cache[dsInfo.Id] = engine - e.XormEngine = engine + engineCache.versions[config.Datasource.Id] = config.Datasource.Version + engineCache.cache[config.Datasource.Id] = engine + queryEndpoint.engine = engine - return nil + return &queryEndpoint, nil } -// Query is a default implementation of the Query method for an SQL data source. -// The caller of this function must implement transformToTimeSeries and transformToTable and -// pass them in as parameters. -func (e *DefaultSqlEngine) Query( - ctx context.Context, - dsInfo *models.DataSource, - tsdbQuery *TsdbQuery, - transformToTimeSeries func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, - transformToTable func(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error, -) (*Response, error) { +const rowLimit = 1000000 + +// Query is the main function for the SqlQueryEndpoint +func (e *sqlQueryEndpoint) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *TsdbQuery) (*Response, error) { result := &Response{ Results: make(map[string]*QueryResult), } - session := e.XormEngine.NewSession() + session := e.engine.NewSession() defer session.Close() db := session.DB() for _, query := range tsdbQuery.Queries { - rawSql := query.Model.Get("rawSql").MustString() - if rawSql == "" { + rawSQL := query.Model.Get("rawSql").MustString() + if rawSQL == "" { continue } queryResult := &QueryResult{Meta: simplejson.New(), RefId: query.RefId} result.Results[query.RefId] = queryResult - rawSql, err := e.MacroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSql) + rawSQL, err := e.macroEngine.Interpolate(query, tsdbQuery.TimeRange, rawSQL) if err != nil { queryResult.Error = err continue } - queryResult.Meta.Set("sql", rawSql) + queryResult.Meta.Set("sql", rawSQL) - rows, err := db.Query(rawSql) + rows, err := db.Query(rawSQL) if err != nil { queryResult.Error = err continue @@ -121,13 +146,13 @@ func (e *DefaultSqlEngine) Query( switch format { case "time_series": - err := transformToTimeSeries(query, rows, queryResult, tsdbQuery) + err := e.transformToTimeSeries(query, rows, queryResult, tsdbQuery) if err != nil { queryResult.Error = err continue } case "table": - err := transformToTable(query, rows, queryResult, tsdbQuery) + err := e.transformToTable(query, rows, queryResult, tsdbQuery) if err != nil { queryResult.Error = err continue @@ -138,6 +163,256 @@ func (e *DefaultSqlEngine) Query( return result, nil } +func (e *sqlQueryEndpoint) transformToTable(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { + columnNames, err := rows.Columns() + columnCount := len(columnNames) + + if err != nil { + return err + } + + rowCount := 0 + timeIndex := -1 + + table := &Table{ + Columns: make([]TableColumn, columnCount), + Rows: make([]RowValues, 0), + } + + for i, name := range columnNames { + table.Columns[i].Text = name + + for _, tc := range e.timeColumnNames { + if name == tc { + timeIndex = i + break + } + } + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + for ; rows.Next(); rowCount++ { + if rowCount > rowLimit { + return fmt.Errorf("query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.rowTransformer.Transform(columnTypes, rows) + if err != nil { + return err + } + + // converts column named time to unix timestamp in milliseconds + // to make native mssql datetime types and epoch dates work in + // annotation and table queries. + ConvertSqlTimeColumnToEpochMs(values, timeIndex) + table.Rows = append(table.Rows, values) + } + + result.Tables = append(result.Tables, table) + result.Meta.Set("rowCount", rowCount) + return nil +} + +func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, result *QueryResult, tsdbQuery *TsdbQuery) error { + pointsBySeries := make(map[string]*TimeSeries) + seriesByQueryOrder := list.New() + + columnNames, err := rows.Columns() + if err != nil { + return err + } + + columnTypes, err := rows.ColumnTypes() + if err != nil { + return err + } + + rowCount := 0 + timeIndex := -1 + metricIndex := -1 + metricPrefix := false + var metricPrefixValue string + + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present + for i, col := range columnNames { + for _, tc := range e.timeColumnNames { + if col == tc { + timeIndex = i + continue + } + } + switch col { + case "metric": + metricIndex = i + default: + if metricIndex == -1 { + columnType := columnTypes[i].DatabaseTypeName() + + for _, mct := range e.metricColumnTypes { + if columnType == mct { + metricIndex = i + continue + } + } + } + } + } + + // use metric column as prefix with multiple value columns + if metricIndex != -1 && len(columnNames) > 3 { + metricPrefix = true + } + + if timeIndex == -1 { + return fmt.Errorf("Found no column named %s", strings.Join(e.timeColumnNames, " or ")) + } + + fillMissing := query.Model.Get("fill").MustBool(false) + var fillInterval float64 + fillValue := null.Float{} + fillPrevious := false + + if fillMissing { + fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 + switch query.Model.Get("fillMode").MustString() { + case "null": + case "previous": + fillPrevious = true + case "value": + fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() + fillValue.Valid = true + } + } + + for rows.Next() { + var timestamp float64 + var value null.Float + var metric string + + if rowCount > rowLimit { + return fmt.Errorf("query row limit exceeded, limit %d", rowLimit) + } + + values, err := e.rowTransformer.Transform(columnTypes, rows) + if err != nil { + return err + } + + // converts column named time to unix timestamp in milliseconds to make + // native mysql datetime types and epoch dates work in + // annotation and table queries. + ConvertSqlTimeColumnToEpochMs(values, timeIndex) + + switch columnValue := values[timeIndex].(type) { + case int64: + timestamp = float64(columnValue) + case float64: + timestamp = columnValue + default: + return fmt.Errorf("Invalid type for column time, must be of type timestamp or unix timestamp, got: %T %v", columnValue, columnValue) + } + + if metricIndex >= 0 { + if columnValue, ok := values[metricIndex].(string); ok { + if metricPrefix { + metricPrefixValue = columnValue + } else { + metric = columnValue + } + } else { + return fmt.Errorf("Column metric must be of type %s. metric column name: %s type: %s but datatype is %T", strings.Join(e.metricColumnTypes, ", "), columnNames[metricIndex], columnTypes[metricIndex].DatabaseTypeName(), values[metricIndex]) + } + } + + for i, col := range columnNames { + if i == timeIndex || i == metricIndex { + continue + } + + if value, err = ConvertSqlValueColumnToFloat(col, values[i]); err != nil { + return err + } + + if metricIndex == -1 { + metric = col + } else if metricPrefix { + metric = metricPrefixValue + " " + col + } + + series, exist := pointsBySeries[metric] + if !exist { + series = &TimeSeries{Name: metric} + pointsBySeries[metric] = series + seriesByQueryOrder.PushBack(metric) + } + + if fillMissing { + var intervalStart float64 + if !exist { + intervalStart = float64(tsdbQuery.TimeRange.MustGetFrom().UnixNano() / 1e6) + } else { + intervalStart = series.Points[len(series.Points)-1][1].Float64 + fillInterval + } + + if fillPrevious { + if len(series.Points) > 0 { + fillValue = series.Points[len(series.Points)-1][0] + } else { + fillValue.Valid = false + } + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + + for i := intervalStart; i < timestamp; i += fillInterval { + series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + + series.Points = append(series.Points, TimePoint{value, null.FloatFrom(timestamp)}) + + e.log.Debug("Rows", "metric", metric, "time", timestamp, "value", value) + } + } + + for elem := seriesByQueryOrder.Front(); elem != nil; elem = elem.Next() { + key := elem.Value.(string) + result.Series = append(result.Series, pointsBySeries[key]) + + if fillMissing { + series := pointsBySeries[key] + // fill in values from last fetched value till interval end + intervalStart := series.Points[len(series.Points)-1][1].Float64 + intervalEnd := float64(tsdbQuery.TimeRange.MustGetTo().UnixNano() / 1e6) + + if fillPrevious { + if len(series.Points) > 0 { + fillValue = series.Points[len(series.Points)-1][0] + } else { + fillValue.Valid = false + } + } + + // align interval start + intervalStart = math.Floor(intervalStart/fillInterval) * fillInterval + for i := intervalStart + fillInterval; i < intervalEnd; i += fillInterval { + series.Points = append(series.Points, TimePoint{fillValue, null.FloatFrom(i)}) + rowCount++ + } + } + } + + result.Meta.Set("rowCount", rowCount) + return nil +} + // ConvertSqlTimeColumnToEpochMs converts column named time to unix timestamp in milliseconds // to make native datetime types and epoch dates work in annotation and table queries. func ConvertSqlTimeColumnToEpochMs(values RowValues, timeIndex int) { @@ -294,3 +569,23 @@ func ConvertSqlValueColumnToFloat(columnName string, columnValue interface{}) (n return value, nil } + +func SetupFillmode(query *Query, interval time.Duration, fillmode string) error { + query.Model.Set("fill", true) + query.Model.Set("fillInterval", interval.Seconds()) + switch fillmode { + case "NULL": + query.Model.Set("fillMode", "null") + case "previous": + query.Model.Set("fillMode", "previous") + default: + query.Model.Set("fillMode", "value") + floatVal, err := strconv.ParseFloat(fillmode, 64) + if err != nil { + return fmt.Errorf("error parsing fill value %v", fillmode) + } + query.Model.Set("fillValue", floatVal) + } + + return nil +} diff --git a/pkg/tsdb/testdata/testdata.go b/pkg/tsdb/testdata/testdata.go index a1ab250ad37..c2c2ea3f696 100644 --- a/pkg/tsdb/testdata/testdata.go +++ b/pkg/tsdb/testdata/testdata.go @@ -21,7 +21,7 @@ func NewTestDataExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, err } func init() { - tsdb.RegisterTsdbQueryEndpoint("grafana-testdata-datasource", NewTestDataExecutor) + tsdb.RegisterTsdbQueryEndpoint("testdata", NewTestDataExecutor) } func (e *TestDataExecutor) Query(ctx context.Context, dsInfo *models.DataSource, tsdbQuery *tsdb.TsdbQuery) (*tsdb.Response, error) { diff --git a/pkg/util/url.go b/pkg/util/url.go index c82dcef67c5..fad2d79a6d0 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -10,7 +10,7 @@ type UrlQueryReader struct { } func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) { - u, err := url.ParseQuery(urlInfo.String()) + u, err := url.ParseQuery(urlInfo.RawQuery) if err != nil { return nil, err } diff --git a/pkg/util/url_test.go b/pkg/util/url_test.go index 4dd221b9e0b..ee29956f60d 100644 --- a/pkg/util/url_test.go +++ b/pkg/util/url_test.go @@ -4,6 +4,7 @@ import ( "testing" . "github.com/smartystreets/goconvey/convey" + "net/url" ) func TestUrl(t *testing.T) { @@ -43,4 +44,30 @@ func TestUrl(t *testing.T) { So(result, ShouldEqual, "http://localhost:8080/api/") }) + + Convey("When joining two urls where lefthand side has a trailing slash and righthand side has preceding slash", t, func() { + result := JoinUrlFragments("http://localhost:8080/", "/api/") + + So(result, ShouldEqual, "http://localhost:8080/api/") + }) +} + +func TestNewUrlQueryReader(t *testing.T) { + u, _ := url.Parse("http://www.abc.com/foo?bar=baz&bar2=baz2") + uqr, _ := NewUrlQueryReader(u) + + Convey("when trying to retrieve the first query value", t, func() { + result := uqr.Get("bar", "foodef") + So(result, ShouldEqual, "baz") + }) + + Convey("when trying to retrieve the second query value", t, func() { + result := uqr.Get("bar2", "foodef") + So(result, ShouldEqual, "baz2") + }) + + Convey("when trying to retrieve from a non-existent key, the default value is returned", t, func() { + result := uqr.Get("bar3", "foodef") + So(result, ShouldEqual, "foodef") + }) } diff --git a/pkg/util/validation_test.go b/pkg/util/validation_test.go new file mode 100644 index 00000000000..124da1b744b --- /dev/null +++ b/pkg/util/validation_test.go @@ -0,0 +1,22 @@ +package util + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestIsEmail(t *testing.T) { + + Convey("When validating a string that is a valid email", t, func() { + result := IsEmail("abc@def.com") + + So(result, ShouldEqual, true) + }) + + Convey("When validating a string that is not a valid email", t, func() { + result := IsEmail("abcdef.com") + + So(result, ShouldEqual, false) + }) +} diff --git a/public/app/app.ts b/public/app/app.ts index 9ac76b8ec91..d9e31018af9 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -53,7 +53,7 @@ export class GrafanaApp { } init() { - var app = angular.module('grafana', []); + const app = angular.module('grafana', []); moment.locale(config.bootData.user.locale); @@ -77,7 +77,7 @@ export class GrafanaApp { '$delegate', '$templateCache', function($delegate, $templateCache) { - var get = $delegate.get; + const get = $delegate.get; $delegate.get = function(url, config) { if (url.match(/\.html$/)) { // some template's already exist in the cache @@ -105,10 +105,10 @@ export class GrafanaApp { 'react', ]; - var module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; + const module_types = ['controllers', 'directives', 'factories', 'services', 'filters', 'routes']; _.each(module_types, type => { - var moduleName = 'grafana.' + type; + const moduleName = 'grafana.' + type; this.useModule(angular.module(moduleName, [])); }); @@ -119,7 +119,7 @@ export class GrafanaApp { coreModule.config(setupAngularRoutes); registerAngularDirectives(); - var preBootRequires = [System.import('app/features/all')]; + const preBootRequires = [System.import('app/features/all')]; Promise.all(preBootRequires) .then(() => { diff --git a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx similarity index 96% rename from public/app/containers/AlertRuleList/AlertRuleList.jest.tsx rename to public/app/containers/AlertRuleList/AlertRuleList.test.tsx index eac18a6c69d..f88ff4522d4 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.jest.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.test.tsx @@ -46,7 +46,7 @@ describe('AlertRuleList', () => { it('should render 1 rule', () => { page.update(); - let ruleNode = page.find('.alert-rule-item'); + const ruleNode = page.find('.alert-rule-item'); expect(toJson(ruleNode)).toMatchSnapshot(); }); diff --git a/public/app/containers/AlertRuleList/AlertRuleList.tsx b/public/app/containers/AlertRuleList/AlertRuleList.tsx index b61c7fbaac3..668136dee6f 100644 --- a/public/app/containers/AlertRuleList/AlertRuleList.tsx +++ b/public/app/containers/AlertRuleList/AlertRuleList.tsx @@ -3,14 +3,14 @@ import { hot } from 'react-hot-loader'; import classNames from 'classnames'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; -import { IAlertRule } from 'app/stores/AlertListStore/AlertListStore'; +import { AlertRule } from 'app/stores/AlertListStore/AlertListStore'; import appEvents from 'app/core/app_events'; -import IContainerProps from 'app/containers/IContainerProps'; +import ContainerProps from 'app/containers/ContainerProps'; import Highlighter from 'react-highlight-words'; @inject('view', 'nav', 'alertList') @observer -export class AlertRuleList extends React.Component { +export class AlertRuleList extends React.Component { stateFilters = [ { text: 'All', value: 'all' }, { text: 'OK', value: 'ok' }, @@ -109,7 +109,7 @@ function AlertStateFilterOption({ text, value }) { } export interface AlertRuleItemProps { - rule: IAlertRule; + rule: AlertRule; search: string; } @@ -132,13 +132,13 @@ export class AlertRuleItem extends React.Component { render() { const { rule } = this.props; - let stateClass = classNames({ + const stateClass = classNames({ fa: true, 'fa-play': rule.isPaused, 'fa-pause': !rule.isPaused, }); - let ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; return (
  • diff --git a/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap b/public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap similarity index 100% rename from public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.jest.tsx.snap rename to public/app/containers/AlertRuleList/__snapshots__/AlertRuleList.test.tsx.snap diff --git a/public/app/containers/IContainerProps.ts b/public/app/containers/ContainerProps.ts similarity index 92% rename from public/app/containers/IContainerProps.ts rename to public/app/containers/ContainerProps.ts index 6e790cee06d..97889278fdc 100644 --- a/public/app/containers/IContainerProps.ts +++ b/public/app/containers/ContainerProps.ts @@ -6,7 +6,7 @@ import { AlertListStore } from './../stores/AlertListStore/AlertListStore'; import { ViewStore } from './../stores/ViewStore/ViewStore'; import { FolderStore } from './../stores/FolderStore/FolderStore'; -interface IContainerProps { +interface ContainerProps { search: typeof SearchStore.Type; serverStats: typeof ServerStatsStore.Type; nav: typeof NavStore.Type; @@ -17,4 +17,4 @@ interface IContainerProps { backendSrv: any; } -export default IContainerProps; +export default ContainerProps; diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index deebe84f2c8..92712709858 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -1,16 +1,33 @@ import React from 'react'; import { hot } from 'react-hot-loader'; +import Select from 'react-select'; + +import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; +import store from 'app/core/store'; import TimeSeries from 'app/core/time_series2'; +import { decodePathComponent } from 'app/core/utils/location_util'; +import { parse as parseDate } from 'app/core/utils/datemath'; import ElapsedTime from './ElapsedTime'; import QueryRows from './QueryRows'; import Graph from './Graph'; +import Logs from './Logs'; import Table from './Table'; import TimePicker, { DEFAULT_RANGE } from './TimePicker'; -import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; -import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; -import { decodePathComponent } from 'app/core/utils/location_util'; +import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; + +const MAX_HISTORY_ITEMS = 100; + +function makeHints(hints) { + const hintsByIndex = []; + hints.forEach(hint => { + if (hint) { + hintsByIndex[hint.index] = hint; + } + }); + return hintsByIndex; +} function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { @@ -30,75 +47,151 @@ function makeTimeSeriesList(dataList, options) { }); } -function parseInitialState(initial) { - try { - const parsed = JSON.parse(decodePathComponent(initial)); - return { - queries: parsed.queries.map(q => q.query), - range: parsed.range, - }; - } catch (e) { - console.error(e); - return { queries: [], range: DEFAULT_RANGE }; +function parseUrlState(initial: string | undefined) { + if (initial) { + try { + const parsed = JSON.parse(decodePathComponent(initial)); + return { + datasource: parsed.datasource, + queries: parsed.queries.map(q => q.query), + range: parsed.range, + }; + } catch (e) { + console.error(e); + } } + return { datasource: null, queries: [], range: DEFAULT_RANGE }; } -interface IExploreState { +interface ExploreState { datasource: any; datasourceError: any; - datasourceLoading: any; + datasourceLoading: boolean | null; + datasourceMissing: boolean; graphResult: any; + history: any[]; + initialDatasource?: string; latency: number; loading: any; - queries: any; - queryError: any; + logsResult: any; + queries: any[]; + queryErrors: any[]; + queryHints: any[]; range: any; requestOptions: any; showingGraph: boolean; + showingLogs: boolean; showingTable: boolean; + supportsGraph: boolean | null; + supportsLogs: boolean | null; + supportsTable: boolean | null; tableResult: any; } -// @observer -export class Explore extends React.Component { - datasourceSrv: DatasourceSrv; +export class Explore extends React.Component { + el: any; constructor(props) { super(props); - const { range, queries } = parseInitialState(props.routeParams.initial); + const initialState: ExploreState = props.initialState; + const { datasource, queries, range } = parseUrlState(props.routeParams.state); this.state = { datasource: null, datasourceError: null, - datasourceLoading: true, + datasourceLoading: null, + datasourceMissing: false, graphResult: null, + initialDatasource: datasource, + history: [], latency: 0, loading: false, + logsResult: null, queries: ensureQueries(queries), - queryError: null, + queryErrors: [], + queryHints: [], range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, + showingLogs: true, showingTable: true, + supportsGraph: null, + supportsLogs: null, + supportsTable: null, tableResult: null, - ...props.initialState, + ...initialState, }; } async componentDidMount() { - const datasource = await this.props.datasourceSrv.get(); - const testResult = await datasource.testDatasource(); - if (testResult.status === 'success') { - this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit()); + const { datasourceSrv } = this.props; + const { initialDatasource } = this.state; + if (!datasourceSrv) { + throw new Error('No datasource service passed as props.'); + } + const datasources = datasourceSrv.getExploreSources(); + if (datasources.length > 0) { + this.setState({ datasourceLoading: true }); + // Priority: datasource in url, default datasource, first explore datasource + let datasource; + if (initialDatasource) { + datasource = await datasourceSrv.get(initialDatasource); + } else { + datasource = await datasourceSrv.get(); + } + if (!datasource.meta.explore) { + datasource = await datasourceSrv.get(datasources[0].name); + } + this.setDatasource(datasource); } else { - this.setState({ datasource: null, datasourceError: testResult.message, datasourceLoading: false }); + this.setState({ datasourceMissing: true }); } } componentDidCatch(error) { + this.setState({ datasourceError: error }); console.error(error); } - handleAddQueryRow = index => { + async setDatasource(datasource) { + const supportsGraph = datasource.meta.metrics; + const supportsLogs = datasource.meta.logs; + const supportsTable = datasource.meta.metrics; + const datasourceId = datasource.meta.id; + let datasourceError = null; + + try { + const testResult = await datasource.testDatasource(); + datasourceError = testResult.status === 'success' ? null : testResult.message; + } catch (error) { + datasourceError = (error && error.statusText) || error; + } + + const historyKey = `grafana.explore.history.${datasourceId}`; + const history = store.getObject(historyKey, []); + + if (datasource.init) { + datasource.init(); + } + + this.setState( + { + datasource, + datasourceError, + history, + supportsGraph, + supportsLogs, + supportsTable, + datasourceLoading: false, + }, + () => datasourceError === null && this.onSubmit() + ); + } + + getRef = el => { + this.el = el; + }; + + onAddQueryRow = index => { const { queries } = this.state; const nextQueries = [ ...queries.slice(0, index + 1), @@ -108,115 +201,261 @@ export class Explore extends React.Component { this.setState({ queries: nextQueries }); }; - handleChangeQuery = (query, index) => { + onChangeDatasource = async option => { + this.setState({ + datasource: null, + datasourceError: null, + datasourceLoading: true, + graphResult: null, + latency: 0, + logsResult: null, + queryErrors: [], + queryHints: [], + tableResult: null, + }); + const datasource = await this.props.datasourceSrv.get(option.value); + this.setDatasource(datasource); + }; + + onChangeQuery = (value: string, index: number, override?: boolean) => { const { queries } = this.state; + let { queryErrors, queryHints } = this.state; + const prevQuery = queries[index]; + const edited = override ? false : prevQuery.query !== value; const nextQuery = { ...queries[index], - query, + edited, + query: value, }; const nextQueries = [...queries]; nextQueries[index] = nextQuery; - this.setState({ queries: nextQueries }); + if (override) { + queryErrors = []; + queryHints = []; + } + this.setState( + { + queryErrors, + queryHints, + queries: nextQueries, + }, + override ? () => this.onSubmit() : undefined + ); }; - handleChangeTime = nextRange => { + onChangeTime = nextRange => { const range = { from: nextRange.from, to: nextRange.to, }; - this.setState({ range }, () => this.handleSubmit()); + this.setState({ range }, () => this.onSubmit()); }; - handleClickCloseSplit = () => { + onClickClear = () => { + this.setState({ + graphResult: null, + logsResult: null, + latency: 0, + queries: ensureQueries(), + queryErrors: [], + queryHints: [], + tableResult: null, + }); + }; + + onClickCloseSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { onChangeSplit(false); } }; - handleClickGraphButton = () => { + onClickGraphButton = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; - handleClickSplit = () => { + onClickLogsButton = () => { + this.setState(state => ({ showingLogs: !state.showingLogs })); + }; + + onClickSplit = () => { const { onChangeSplit } = this.props; + const state = { ...this.state }; + state.queries = state.queries.map(({ edited, ...rest }) => rest); if (onChangeSplit) { - onChangeSplit(true, this.state); + onChangeSplit(true, state); } }; - handleClickTableButton = () => { + onClickTableButton = () => { this.setState(state => ({ showingTable: !state.showingTable })); }; - handleRemoveQueryRow = index => { + onClickTableCell = (columnKey: string, rowValue: string) => { + this.onModifyQueries({ type: 'ADD_FILTER', key: columnKey, value: rowValue }); + }; + + onModifyQueries = (action: object, index?: number) => { + const { datasource, queries } = this.state; + if (datasource && datasource.modifyQuery) { + let nextQueries; + if (index === undefined) { + // Modify all queries + nextQueries = queries.map(q => ({ + ...q, + edited: false, + query: datasource.modifyQuery(q.query, action), + })); + } else { + // Modify query only at index + nextQueries = [ + ...queries.slice(0, index), + { + ...queries[index], + edited: false, + query: datasource.modifyQuery(queries[index].query, action), + }, + ...queries.slice(index + 1), + ]; + } + this.setState({ queries: nextQueries }, () => this.onSubmit()); + } + }; + + onRemoveQueryRow = index => { const { queries } = this.state; if (queries.length <= 1) { return; } const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; - this.setState({ queries: nextQueries }, () => this.handleSubmit()); + this.setState({ queries: nextQueries }, () => this.onSubmit()); }; - handleSubmit = () => { - const { showingGraph, showingTable } = this.state; - if (showingTable) { + onSubmit = () => { + const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; + if (showingTable && supportsTable) { this.runTableQuery(); } - if (showingGraph) { + if (showingGraph && supportsGraph) { this.runGraphQuery(); } + if (showingLogs && supportsLogs) { + this.runLogsQuery(); + } }; - async runGraphQuery() { + onQuerySuccess(datasourceId: string, queries: any[]): void { + // save queries to history + let { history } = this.state; + const { datasource } = this.state; + + if (datasource.meta.id !== datasourceId) { + // Navigated away, queries did not matter + return; + } + + const ts = Date.now(); + queries.forEach(q => { + const { query } = q; + history = [{ query, ts }, ...history]; + }); + + if (history.length > MAX_HISTORY_ITEMS) { + history = history.slice(0, MAX_HISTORY_ITEMS); + } + + // Combine all queries of a datasource type into one history + const historyKey = `grafana.explore.history.${datasourceId}`; + store.setObject(historyKey, history); + this.setState({ history }); + } + + buildQueryOptions(targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, queries, range } = this.state; + const resolution = this.el.offsetWidth; + const absoluteRange = { + from: parseDate(range.from, false), + to: parseDate(range.to, true), + }; + const { interval } = kbn.calculateInterval(absoluteRange, resolution, datasource.interval); + const targets = queries.map(q => ({ + ...targetOptions, + expr: q.query, + })); + return { + interval, + range, + targets, + }; + } + + async runGraphQuery() { + const { datasource, queries } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, graphResult: null, queryError: null }); + this.setState({ latency: 0, loading: true, graphResult: null, queryErrors: [], queryHints: [] }); const now = Date.now(); - const options = buildQueryOptions({ - format: 'time_series', - interval: datasource.interval, - instant: false, - range, - queries: queries.map(q => q.query), - }); + const options = this.buildQueryOptions({ format: 'time_series', instant: false, hinting: true }); try { const res = await datasource.query(options); const result = makeTimeSeriesList(res.data, options); + const queryHints = res.hints ? makeHints(res.hints) : []; const latency = Date.now() - now; - this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + this.setState({ latency, loading: false, graphResult: result, queryHints, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); } } async runTableQuery() { - const { datasource, queries, range } = this.state; + const { datasource, queries } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, queryError: null, tableResult: null }); + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], tableResult: null }); const now = Date.now(); - const options = buildQueryOptions({ + const options = this.buildQueryOptions({ format: 'table', - interval: datasource.interval, instant: true, - range, - queries: queries.map(q => q.query), }); try { const res = await datasource.query(options); const tableModel = res.data[0]; const latency = Date.now() - now; this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; - this.setState({ loading: false, queryError }); + this.setState({ loading: false, queryErrors: [queryError] }); + } + } + + async runLogsQuery() { + const { datasource, queries } = this.state; + if (!hasQuery(queries)) { + return; + } + this.setState({ latency: 0, loading: true, queryErrors: [], queryHints: [], logsResult: null }); + const now = Date.now(); + const options = this.buildQueryOptions({ + format: 'logs', + }); + + try { + const res = await datasource.query(options); + const logsData = res.data; + const latency = Date.now() - now; + this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.setState({ loading: false, queryErrors: [queryError] }); } } @@ -226,29 +465,44 @@ export class Explore extends React.Component { }; render() { - const { position, split } = this.props; + const { datasourceSrv, position, split } = this.props; const { datasource, datasourceError, datasourceLoading, + datasourceMissing, graphResult, + history, latency, loading, + logsResult, queries, - queryError, + queryErrors, + queryHints, range, requestOptions, showingGraph, + showingLogs, showingTable, + supportsGraph, + supportsLogs, + supportsTable, tableResult, } = this.state; const showingBoth = showingGraph && showingTable; const graphHeight = showingBoth ? '200px' : '400px'; const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; + const logsButtonActive = showingLogs ? 'active' : ''; const tableButtonActive = showingBoth || showingTable ? 'active' : ''; const exploreClass = split ? 'explore explore-split' : 'explore'; + const datasources = datasourceSrv.getExploreSources().map(ds => ({ + value: ds.name, + label: ds.name, + })); + const selectedDatasource = datasource ? datasource.name : undefined; + return ( -
    +
    {position === 'left' ? (
    @@ -259,30 +513,39 @@ export class Explore extends React.Component {
    ) : (
    -
    )} + {!datasourceMissing ? ( +
    + +
    + +
    + +
    +
    +
    + + + {groups.length === 0 && + !isAdding && ( +
    +
    There are no external groups to sync with
    + +
    + {headerTooltip} + + Learn more + +
    +
    + )} + + {groups.length > 0 && ( +
    + + + + + + + {groups.map(group => this.renderGroup(group))} +
    External Group ID +
    +
    + )} +
    + ); + } +} + +export default hot(module)(TeamGroupSync); diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx new file mode 100644 index 00000000000..d0feee75184 --- /dev/null +++ b/public/app/containers/Teams/TeamList.tsx @@ -0,0 +1,111 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { inject, observer } from 'mobx-react'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import { NavStore } from 'app/stores/NavStore/NavStore'; +import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; + +interface Props { + nav: typeof NavStore.Type; + teams: typeof TeamsStore.Type; + backendSrv: BackendSrv; +} + +@inject('nav', 'teams') +@observer +export class TeamList extends React.Component { + constructor(props) { + super(props); + + this.props.nav.load('cfg', 'teams'); + this.fetchTeams(); + } + + fetchTeams() { + this.props.teams.loadTeams(); + } + + deleteTeam(team: Team) { + this.props.backendSrv.delete('/api/teams/' + team.id).then(this.fetchTeams.bind(this)); + } + + onSearchQueryChange = evt => { + this.props.teams.setSearchQuery(evt.target.value); + }; + + renderTeamMember(team: Team): JSX.Element { + const teamUrl = `org/teams/edit/${team.id}`; + + return ( + + + + + + + + {team.name} + + + {team.email} + + + {team.memberCount} + + + this.deleteTeam(team)} /> + + + ); + } + + render() { + const { nav, teams } = this.props; + return ( +
    + +
    +
    +
    + +
    + + + +
    + + + + + + + + + {teams.filteredTeams.map(team => this.renderTeamMember(team))} +
    + NameEmailMembers +
    +
    +
    +
    + ); + } +} + +export default hot(module)(TeamList); diff --git a/public/app/containers/Teams/TeamMembers.tsx b/public/app/containers/Teams/TeamMembers.tsx new file mode 100644 index 00000000000..b06a547063a --- /dev/null +++ b/public/app/containers/Teams/TeamMembers.tsx @@ -0,0 +1,135 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { observer } from 'mobx-react'; +import { Team, TeamMember } from 'app/stores/TeamsStore/TeamsStore'; +import SlideDown from 'app/core/components/Animations/SlideDown'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; + +interface Props { + team: Team; +} + +interface State { + isAdding: boolean; + newTeamMember?: User; +} + +@observer +export class TeamMembers extends React.Component { + constructor(props) { + super(props); + this.state = { isAdding: false, newTeamMember: null }; + } + + componentDidMount() { + this.props.team.loadMembers(); + } + + onSearchQueryChange = evt => { + this.props.team.setSearchQuery(evt.target.value); + }; + + removeMember(member: TeamMember) { + this.props.team.removeMember(member); + } + + removeMemberConfirmed(member: TeamMember) { + this.props.team.removeMember(member); + } + + renderMember(member: TeamMember) { + return ( + + + + + {member.login} + {member.email} + + this.removeMember(member)} /> + + + ); + } + + onToggleAdding = () => { + this.setState({ isAdding: !this.state.isAdding }); + }; + + onUserSelected = (user: User) => { + this.setState({ newTeamMember: user }); + }; + + onAddUserToTeam = async () => { + await this.props.team.addMember(this.state.newTeamMember.id); + await this.props.team.loadMembers(); + this.setState({ newTeamMember: null }); + }; + + render() { + const { newTeamMember, isAdding } = this.state; + const members = this.props.team.filteredMembers; + const newTeamMemberValue = newTeamMember && newTeamMember.id.toString(); + const { team } = this.props; + + return ( +
    +
    +
    + +
    + +
    + + +
    + + +
    + +
    Add Team Member
    +
    + + + {this.state.newTeamMember && ( + + )} +
    +
    +
    + +
    + + + + + + + + {members.map(member => this.renderMember(member))} +
    + NameEmail +
    +
    +
    + ); + } +} + +export default hot(module)(TeamMembers); diff --git a/public/app/containers/Teams/TeamPages.tsx b/public/app/containers/Teams/TeamPages.tsx new file mode 100644 index 00000000000..2abc9c51535 --- /dev/null +++ b/public/app/containers/Teams/TeamPages.tsx @@ -0,0 +1,77 @@ +import React from 'react'; +import _ from 'lodash'; +import { hot } from 'react-hot-loader'; +import { inject, observer } from 'mobx-react'; +import config from 'app/core/config'; +import PageHeader from 'app/core/components/PageHeader/PageHeader'; +import { NavStore } from 'app/stores/NavStore/NavStore'; +import { TeamsStore, Team } from 'app/stores/TeamsStore/TeamsStore'; +import { ViewStore } from 'app/stores/ViewStore/ViewStore'; +import TeamMembers from './TeamMembers'; +import TeamSettings from './TeamSettings'; +import TeamGroupSync from './TeamGroupSync'; + +interface Props { + nav: typeof NavStore.Type; + teams: typeof TeamsStore.Type; + view: typeof ViewStore.Type; +} + +@inject('nav', 'teams', 'view') +@observer +export class TeamPages extends React.Component { + isSyncEnabled: boolean; + currentPage: string; + + constructor(props) { + super(props); + + this.isSyncEnabled = config.buildInfo.isEnterprise; + this.currentPage = this.getCurrentPage(); + + this.loadTeam(); + } + + async loadTeam() { + const { teams, nav, view } = this.props; + + await teams.loadById(view.routeParams.get('id')); + + nav.initTeamPage(this.getCurrentTeam(), this.currentPage, this.isSyncEnabled); + } + + getCurrentTeam(): Team { + const { teams, view } = this.props; + return teams.map.get(view.routeParams.get('id')); + } + + getCurrentPage() { + const pages = ['members', 'settings', 'groupsync']; + const currentPage = this.props.view.routeParams.get('page'); + return _.includes(pages, currentPage) ? currentPage : pages[0]; + } + + render() { + const { nav } = this.props; + const currentTeam = this.getCurrentTeam(); + + if (!nav.main) { + return null; + } + + return ( +
    + + {currentTeam && ( +
    + {this.currentPage === 'members' && } + {this.currentPage === 'settings' && } + {this.currentPage === 'groupsync' && this.isSyncEnabled && } +
    + )} +
    + ); + } +} + +export default hot(module)(TeamPages); diff --git a/public/app/containers/Teams/TeamSettings.tsx b/public/app/containers/Teams/TeamSettings.tsx new file mode 100644 index 00000000000..0de60a0b16c --- /dev/null +++ b/public/app/containers/Teams/TeamSettings.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { hot } from 'react-hot-loader'; +import { observer } from 'mobx-react'; +import { Team } from 'app/stores/TeamsStore/TeamsStore'; +import { Label } from 'app/core/components/Forms/Forms'; + +interface Props { + team: Team; +} + +@observer +export class TeamSettings extends React.Component { + constructor(props) { + super(props); + } + + onChangeName = evt => { + this.props.team.setName(evt.target.value); + }; + + onChangeEmail = evt => { + this.props.team.setEmail(evt.target.value); + }; + + onUpdate = evt => { + evt.preventDefault(); + this.props.team.update(); + }; + + render() { + return ( +
    +

    Team Settings

    +
    +
    + + +
    +
    + + +
    + +
    + +
    +
    +
    + ); + } +} + +export default hot(module)(TeamSettings); diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index ace0eb00b07..a4439509f8e 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; import LoginBackground from './components/Login/LoginBackground'; import { SearchResult } from './components/search/SearchResult'; import { TagFilter } from './components/TagFilter/TagFilter'; -import UserPicker from './components/Picker/UserPicker'; import DashboardPermissions from './components/Permissions/DashboardPermissions'; export function registerAngularDirectives() { @@ -19,6 +18,5 @@ export function registerAngularDirectives() { ['onSelect', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); - react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']); react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']); } diff --git a/public/app/core/components/DeleteButton/DeleteButton.test.tsx b/public/app/core/components/DeleteButton/DeleteButton.test.tsx new file mode 100644 index 00000000000..12acadee18a --- /dev/null +++ b/public/app/core/components/DeleteButton/DeleteButton.test.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import DeleteButton from './DeleteButton'; +import { shallow } from 'enzyme'; + +describe('DeleteButton', () => { + let wrapper; + let deleted; + + beforeAll(() => { + deleted = false; + + function deleteItem() { + deleted = true; + } + wrapper = shallow( deleteItem()} />); + }); + + it('should show confirm delete when clicked', () => { + expect(wrapper.state().showConfirm).toBe(false); + wrapper.find('.delete-button').simulate('click'); + expect(wrapper.state().showConfirm).toBe(true); + }); + + it('should hide confirm delete when clicked', () => { + wrapper.find('.delete-button').simulate('click'); + expect(wrapper.state().showConfirm).toBe(true); + wrapper + .find('.confirm-delete') + .find('.btn') + .at(0) + .simulate('click'); + expect(wrapper.state().showConfirm).toBe(false); + }); + + it('should show confirm delete when clicked', () => { + expect(deleted).toBe(false); + wrapper + .find('.confirm-delete') + .find('.btn') + .at(1) + .simulate('click'); + expect(deleted).toBe(true); + }); +}); diff --git a/public/app/core/components/DeleteButton/DeleteButton.tsx b/public/app/core/components/DeleteButton/DeleteButton.tsx new file mode 100644 index 00000000000..a83ce6097ad --- /dev/null +++ b/public/app/core/components/DeleteButton/DeleteButton.tsx @@ -0,0 +1,66 @@ +import React, { PureComponent } from 'react'; + +export interface DeleteButtonProps { + onConfirmDelete(); +} + +export interface DeleteButtonStates { + showConfirm: boolean; +} + +export default class DeleteButton extends PureComponent { + state: DeleteButtonStates = { + showConfirm: false, + }; + + onClickDelete = event => { + if (event) { + event.preventDefault(); + } + + this.setState({ + showConfirm: true, + }); + }; + + onClickCancel = event => { + if (event) { + event.preventDefault(); + } + this.setState({ + showConfirm: false, + }); + }; + + render() { + const onClickConfirm = this.props.onConfirmDelete; + let showConfirm; + let showDeleteButton; + + if (this.state.showConfirm) { + showConfirm = 'show'; + showDeleteButton = 'hide'; + } else { + showConfirm = 'hide'; + showDeleteButton = 'show'; + } + + return ( + + + + + + + + Cancel + + + Confirm Delete + + + + + ); + } +} diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx similarity index 100% rename from public/app/core/components/EmptyListCTA/EmptyListCTA.jest.tsx rename to public/app/core/components/EmptyListCTA/EmptyListCTA.test.tsx diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index 1583303dfa1..5ece360e36a 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -1,34 +1,37 @@ import React, { Component } from 'react'; -export interface IProps { - model: any; +export interface Props { + model: any; } -class EmptyListCTA extends Component { - render() { - const { - title, - buttonIcon, - buttonLink, - buttonTitle, - proTip, - proTipLink, - proTipLinkTitle, - proTipTarget - } = this.props.model; - return ( -
    -
    {title}
    - {buttonTitle} -
    - ProTip: {proTip} - {proTipLinkTitle} -
    -
    - ); - } +class EmptyListCTA extends Component { + render() { + const { + title, + buttonIcon, + buttonLink, + buttonTitle, + proTip, + proTipLink, + proTipLinkTitle, + proTipTarget, + } = this.props.model; + return ( +
    +
    {title}
    + + + {buttonTitle} + +
    + ProTip: {proTip} + + {proTipLinkTitle} + +
    +
    + ); + } } export default EmptyListCTA; diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap similarity index 100% rename from public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.jest.tsx.snap rename to public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap diff --git a/public/app/core/components/Forms/Forms.tsx b/public/app/core/components/Forms/Forms.tsx new file mode 100644 index 00000000000..543e1a1d6df --- /dev/null +++ b/public/app/core/components/Forms/Forms.tsx @@ -0,0 +1,21 @@ +import React, { SFC, ReactNode } from 'react'; +import Tooltip from '../Tooltip/Tooltip'; + +interface Props { + tooltip?: string; + for?: string; + children: ReactNode; +} + +export const Label: SFC = props => { + return ( + + {props.children} + {props.tooltip && ( + + + + )} + + ); +}; diff --git a/public/app/core/components/PageHeader/PageHeader.jest.tsx b/public/app/core/components/PageHeader/PageHeader.test.tsx similarity index 100% rename from public/app/core/components/PageHeader/PageHeader.jest.tsx rename to public/app/core/components/PageHeader/PageHeader.test.tsx diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index f998cb9981f..b7bef2495bb 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -5,7 +5,7 @@ import classNames from 'classnames'; import appEvents from 'app/core/app_events'; import { toJS } from 'mobx'; -export interface IProps { +export interface Props { model: NavModel; } @@ -15,8 +15,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string }); const gotoUrl = evt => { - var element = evt.target; - var url = element.options[element.selectedIndex].value; + const element = evt.target; + const url = element.options[element.selectedIndex].value; appEvents.emit('location-change', { href: url }); }; @@ -82,7 +82,7 @@ const Navigation = ({ main }: { main: NavModelItem }) => { }; @observer -export default class PageHeader extends React.Component { +export default class PageHeader extends React.Component { constructor(props) { super(props); } diff --git a/public/app/core/components/PasswordStrength.tsx b/public/app/core/components/PasswordStrength.tsx index 8f92b18445c..1d676a00a37 100644 --- a/public/app/core/components/PasswordStrength.tsx +++ b/public/app/core/components/PasswordStrength.tsx @@ -1,32 +1,31 @@ import React from 'react'; -export interface IProps { +export interface Props { password: string; } -export class PasswordStrength extends React.Component { - +export class PasswordStrength extends React.Component { constructor(props) { super(props); } render() { const { password } = this.props; - let strengthText = "strength: strong like a bull."; - let strengthClass = "password-strength-good"; + let strengthText = 'strength: strong like a bull.'; + let strengthClass = 'password-strength-good'; if (!password) { return null; } if (password.length <= 8) { - strengthText = "strength: you can do better."; - strengthClass = "password-strength-ok"; + strengthText = 'strength: you can do better.'; + strengthClass = 'password-strength-ok'; } if (password.length < 4) { - strengthText = "strength: weak sauce."; - strengthClass = "password-strength-bad"; + strengthText = 'strength: weak sauce.'; + strengthClass = 'password-strength-bad'; } return ( @@ -36,5 +35,3 @@ export class PasswordStrength extends React.Component { ); } } - - diff --git a/public/app/core/components/Permissions/AddPermissions.jest.tsx b/public/app/core/components/Permissions/AddPermissions.test.tsx similarity index 68% rename from public/app/core/components/Permissions/AddPermissions.jest.tsx rename to public/app/core/components/Permissions/AddPermissions.test.tsx index fe97c4c7e62..c6d1ab381b8 100644 --- a/public/app/core/components/Permissions/AddPermissions.jest.tsx +++ b/public/app/core/components/Permissions/AddPermissions.test.tsx @@ -1,32 +1,32 @@ -import React from 'react'; +import React from 'react'; +import { shallow } from 'enzyme'; import AddPermissions from './AddPermissions'; import { RootStore } from 'app/stores/RootStore/RootStore'; -import { backendSrv } from 'test/mocks/common'; -import { shallow } from 'enzyme'; +import { getBackendSrv } from 'app/core/services/backend_srv'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([ + { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, + { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, + ]); + }, + post: jest.fn(() => Promise.resolve({})), + }; + }, +})); describe('AddPermissions', () => { let wrapper; let store; let instance; + const backendSrv: any = getBackendSrv(); beforeAll(() => { - backendSrv.get.mockReturnValue( - Promise.resolve([ - { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' }, - { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' }, - ]) - ); - - backendSrv.post = jest.fn(() => Promise.resolve({})); - - store = RootStore.create( - {}, - { - backendSrv: backendSrv, - } - ); - - wrapper = shallow(); + store = RootStore.create({}, { backendSrv: backendSrv }); + wrapper = shallow(); instance = wrapper.instance(); return store.permissions.load(1, true, false); }); @@ -43,8 +43,8 @@ describe('AddPermissions', () => { login: 'user2', }; - instance.typeChanged(evt); - instance.userPicked(userItem); + instance.onTypeChanged(evt); + instance.onUserSelected(userItem); wrapper.update(); @@ -70,8 +70,8 @@ describe('AddPermissions', () => { name: 'ug1', }; - instance.typeChanged(evt); - instance.teamPicked(teamItem); + instance.onTypeChanged(evt); + instance.onTeamSelected(teamItem); wrapper.update(); diff --git a/public/app/core/components/Permissions/AddPermissions.tsx b/public/app/core/components/Permissions/AddPermissions.tsx index 4dcd07ffb48..289e27aa731 100644 --- a/public/app/core/components/Permissions/AddPermissions.tsx +++ b/public/app/core/components/Permissions/AddPermissions.tsx @@ -1,24 +1,19 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import { observer } from 'mobx-react'; import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore'; -import UserPicker, { User } from 'app/core/components/Picker/UserPicker'; -import TeamPicker, { Team } from 'app/core/components/Picker/TeamPicker'; +import { UserPicker, User } from 'app/core/components/Picker/UserPicker'; +import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker'; import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker'; import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore'; -export interface IProps { +export interface Props { permissions: any; - backendSrv: any; } + @observer -class AddPermissions extends Component { +class AddPermissions extends Component { constructor(props) { super(props); - this.userPicked = this.userPicked.bind(this); - this.teamPicked = this.teamPicked.bind(this); - this.permissionPicked = this.permissionPicked.bind(this); - this.typeChanged = this.typeChanged.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); } componentWillMount() { @@ -26,49 +21,49 @@ class AddPermissions extends Component { permissions.resetNewType(); } - typeChanged(evt) { + onTypeChanged = evt => { const { value } = evt.target; const { permissions } = this.props; permissions.setNewType(value); - } + }; - userPicked(user: User) { + onUserSelected = (user: User) => { const { permissions } = this.props; if (!user) { permissions.newItem.setUser(null, null); return; } return permissions.newItem.setUser(user.id, user.login, user.avatarUrl); - } + }; - teamPicked(team: Team) { + onTeamSelected = (team: Team) => { const { permissions } = this.props; if (!team) { permissions.newItem.setTeam(null, null); return; } return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl); - } + }; - permissionPicked(permission: OptionWithDescription) { + onPermissionChanged = (permission: OptionWithDescription) => { const { permissions } = this.props; return permissions.newItem.setPermission(permission.value); - } + }; resetNewType() { const { permissions } = this.props; return permissions.resetNewType(); } - handleSubmit(evt) { + onSubmit = evt => { evt.preventDefault(); const { permissions } = this.props; permissions.addStoreItem(); - } + }; render() { - const { permissions, backendSrv } = this.props; + const { permissions } = this.props; const newItem = permissions.newItem; const pickerClassName = 'width-20'; @@ -79,12 +74,12 @@ class AddPermissions extends Component { -
    -
    Add Permission For
    + +
    Add Permission For
    - {aclTypes.map((option, idx) => { return (
    - + { +export default class DisabledPermissionListItem extends Component { render() { const { item } = this.props; @@ -25,7 +25,7 @@ export default class DisabledPermissionListItem extends Component {
    {}} + onSelected={() => {}} value={item.permission} disabled={true} className={'gf-form-input--form-dropdown-right'} diff --git a/public/app/core/components/Permissions/Permissions.tsx b/public/app/core/components/Permissions/Permissions.tsx index dbdc1682f6b..d17899c891f 100644 --- a/public/app/core/components/Permissions/Permissions.tsx +++ b/public/app/core/components/Permissions/Permissions.tsx @@ -20,7 +20,7 @@ export interface DashboardAcl { sortRank?: number; } -export interface IProps { +export interface Props { dashboardId: number; folderInfo?: FolderInfo; permissions?: any; @@ -29,7 +29,7 @@ export interface IProps { } @observer -class Permissions extends Component { +class Permissions extends Component { constructor(props) { super(props); const { dashboardId, isFolder, folderInfo } = this.props; diff --git a/public/app/core/components/Permissions/PermissionsList.tsx b/public/app/core/components/Permissions/PermissionsList.tsx index a77235ecc30..7e64de012e4 100644 --- a/public/app/core/components/Permissions/PermissionsList.tsx +++ b/public/app/core/components/Permissions/PermissionsList.tsx @@ -4,7 +4,7 @@ import DisabledPermissionsListItem from './DisabledPermissionsListItem'; import { observer } from 'mobx-react'; import { FolderInfo } from './FolderInfo'; -export interface IProps { +export interface Props { permissions: any[]; removeItem: any; permissionChanged: any; @@ -13,7 +13,7 @@ export interface IProps { } @observer -class PermissionsList extends Component { +class PermissionsList extends Component { render() { const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props; diff --git a/public/app/core/components/Permissions/PermissionsListItem.tsx b/public/app/core/components/Permissions/PermissionsListItem.tsx index b0158525d52..a17aa8c04df 100644 --- a/public/app/core/components/Permissions/PermissionsListItem.tsx +++ b/public/app/core/components/Permissions/PermissionsListItem.tsx @@ -68,7 +68,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex, folde
    { +class DescriptionOption extends Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/Picker/DescriptionPicker.tsx b/public/app/core/components/Picker/DescriptionPicker.tsx index 498c0df7ab2..2e53d096e08 100644 --- a/public/app/core/components/Picker/DescriptionPicker.tsx +++ b/public/app/core/components/Picker/DescriptionPicker.tsx @@ -2,9 +2,9 @@ import React, { Component } from 'react'; import Select from 'react-select'; import DescriptionOption from './DescriptionOption'; -export interface IProps { +export interface Props { optionsWithDesc: OptionWithDescription[]; - handlePicked: (permission) => void; + onSelected: (permission) => void; value: number; disabled: boolean; className?: string; @@ -16,14 +16,14 @@ export interface OptionWithDescription { description: string; } -class DescriptionPicker extends Component { +class DescriptionPicker extends Component { constructor(props) { super(props); this.state = {}; } render() { - const { optionsWithDesc, handlePicked, value, disabled, className } = this.props; + const { optionsWithDesc, onSelected, value, disabled, className } = this.props; return (
    @@ -34,7 +34,7 @@ class DescriptionPicker extends Component { clearable={false} labelKey="label" options={optionsWithDesc} - onChange={handlePicked} + onChange={onSelected} className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={DescriptionOption} placeholder="Choose" diff --git a/public/app/core/components/Picker/PickerOption.jest.tsx b/public/app/core/components/Picker/PickerOption.test.tsx similarity index 100% rename from public/app/core/components/Picker/PickerOption.jest.tsx rename to public/app/core/components/Picker/PickerOption.test.tsx diff --git a/public/app/core/components/Picker/PickerOption.tsx b/public/app/core/components/Picker/PickerOption.tsx index 1b32adac572..f30a7c06d10 100644 --- a/public/app/core/components/Picker/PickerOption.tsx +++ b/public/app/core/components/Picker/PickerOption.tsx @@ -1,6 +1,6 @@ import React, { Component } from 'react'; -export interface IProps { +export interface Props { onSelect: any; onFocus: any; option: any; @@ -8,7 +8,7 @@ export interface IProps { className: any; } -class UserPickerOption extends Component { +class UserPickerOption extends Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/Picker/TeamPicker.jest.tsx b/public/app/core/components/Picker/TeamPicker.jest.tsx deleted file mode 100644 index 20b7620e0ac..00000000000 --- a/public/app/core/components/Picker/TeamPicker.jest.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import TeamPicker from './TeamPicker'; - -const model = { - backendSrv: { - get: () => { - return new Promise((resolve, reject) => {}); - }, - }, - handlePicked: () => {}, -}; - -describe('TeamPicker', () => { - it('renders correctly', () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/Picker/TeamPicker.test.tsx b/public/app/core/components/Picker/TeamPicker.test.tsx new file mode 100644 index 00000000000..3db9f7bb4eb --- /dev/null +++ b/public/app/core/components/Picker/TeamPicker.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { TeamPicker } from './TeamPicker'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([]); + }, + }; + }, +})); + +describe('TeamPicker', () => { + it('renders correctly', () => { + const props = { + onSelected: () => {}, + }; + const tree = renderer.create().toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Picker/TeamPicker.tsx b/public/app/core/components/Picker/TeamPicker.tsx index 2dfff1850dd..04f108ff8da 100644 --- a/public/app/core/components/Picker/TeamPicker.tsx +++ b/public/app/core/components/Picker/TeamPicker.tsx @@ -1,18 +1,19 @@ -import React, { Component } from 'react'; +import React, { Component } from 'react'; import Select from 'react-select'; import PickerOption from './PickerOption'; -import withPicker from './withPicker'; import { debounce } from 'lodash'; +import { getBackendSrv } from 'app/core/services/backend_srv'; -export interface IProps { - backendSrv: any; - isLoading: boolean; - toggleLoading: any; - handlePicked: (user) => void; +export interface Props { + onSelected: (team: Team) => void; value?: string; className?: string; } +export interface State { + isLoading; +} + export interface Team { id: number; label: string; @@ -20,13 +21,12 @@ export interface Team { avatarUrl: string; } -class TeamPicker extends Component { +export class TeamPicker extends Component { debouncedSearch: any; - backendSrv: any; constructor(props) { super(props); - this.state = {}; + this.state = { isLoading: false }; this.search = this.search.bind(this); this.debouncedSearch = debounce(this.search, 300, { @@ -36,9 +36,9 @@ class TeamPicker extends Component { } search(query?: string) { - const { toggleLoading, backendSrv } = this.props; + const backendSrv = getBackendSrv(); + this.setState({ isLoading: true }); - toggleLoading(true); return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => { const teams = result.teams.map(team => { return { @@ -49,18 +49,18 @@ class TeamPicker extends Component { }; }); - toggleLoading(false); + this.setState({ isLoading: false }); return { options: teams }; }); } render() { - const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value, className } = this.props; + const { onSelected, value, className } = this.props; + const { isLoading } = this.state; return (
    - { loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." noResultsText="No teams found" - onChange={handlePicked} + onChange={onSelected} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} - placeholder="Choose" + placeholder="Select a team" value={value} autosize={true} /> @@ -80,5 +80,3 @@ class TeamPicker extends Component { ); } } - -export default withPicker(TeamPicker); diff --git a/public/app/core/components/Picker/UserPicker.jest.tsx b/public/app/core/components/Picker/UserPicker.jest.tsx deleted file mode 100644 index 756fa2d9801..00000000000 --- a/public/app/core/components/Picker/UserPicker.jest.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import React from 'react'; -import renderer from 'react-test-renderer'; -import UserPicker from './UserPicker'; - -const model = { - backendSrv: { - get: () => { - return new Promise((resolve, reject) => {}); - }, - }, - handlePicked: () => {}, -}; - -describe('UserPicker', () => { - it('renders correctly', () => { - const tree = renderer.create().toJSON(); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/public/app/core/components/Picker/UserPicker.test.tsx b/public/app/core/components/Picker/UserPicker.test.tsx new file mode 100644 index 00000000000..054ca643700 --- /dev/null +++ b/public/app/core/components/Picker/UserPicker.test.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import renderer from 'react-test-renderer'; +import { UserPicker } from './UserPicker'; + +jest.mock('app/core/services/backend_srv', () => ({ + getBackendSrv: () => { + return { + get: () => { + return Promise.resolve([]); + }, + }; + }, +})); + +describe('UserPicker', () => { + it('renders correctly', () => { + const tree = renderer.create( {}} />).toJSON(); + expect(tree).toMatchSnapshot(); + }); +}); diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index 77bf6c1fe15..e50513c44e1 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -1,18 +1,19 @@ import React, { Component } from 'react'; import Select from 'react-select'; import PickerOption from './PickerOption'; -import withPicker from './withPicker'; import { debounce } from 'lodash'; +import { getBackendSrv } from 'app/core/services/backend_srv'; -export interface IProps { - backendSrv: any; - isLoading: boolean; - toggleLoading: any; - handlePicked: (user) => void; +export interface Props { + onSelected: (user: User) => void; value?: string; className?: string; } +export interface State { + isLoading: boolean; +} + export interface User { id: number; label: string; @@ -20,13 +21,12 @@ export interface User { login: string; } -class UserPicker extends Component { +export class UserPicker extends Component { debouncedSearch: any; - backendSrv: any; constructor(props) { super(props); - this.state = {}; + this.state = { isLoading: false }; this.search = this.search.bind(this); this.debouncedSearch = debounce(this.search, 300, { @@ -36,29 +36,34 @@ class UserPicker extends Component { } search(query?: string) { - const { toggleLoading, backendSrv } = this.props; + const backendSrv = getBackendSrv(); - toggleLoading(true); - return backendSrv.get(`/api/org/users?query=${query}&limit=10`).then(result => { - const users = result.map(user => { + this.setState({ isLoading: true }); + + return backendSrv + .get(`/api/org/users?query=${query}&limit=10`) + .then(result => { return { - id: user.userId, - label: `${user.login} - ${user.email}`, - avatarUrl: user.avatarUrl, - login: user.login, + options: result.map(user => ({ + id: user.userId, + label: `${user.login} - ${user.email}`, + avatarUrl: user.avatarUrl, + login: user.login, + })), }; + }) + .finally(() => { + this.setState({ isLoading: false }); }); - toggleLoading(false); - return { options: users }; - }); } render() { - const AsyncComponent = this.state.creatable ? Select.AsyncCreatable : Select.Async; - const { isLoading, handlePicked, value, className } = this.props; + const { value, className } = this.props; + const { isLoading } = this.state; + return (
    - { loadOptions={this.debouncedSearch} loadingPlaceholder="Loading..." noResultsText="No users found" - onChange={handlePicked} + onChange={this.props.onSelected} className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`} optionComponent={PickerOption} - placeholder="Choose" + placeholder="Select user" value={value} autosize={true} /> @@ -78,5 +83,3 @@ class UserPicker extends Component { ); } } - -export default withPicker(UserPicker); diff --git a/public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/PickerOption.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap diff --git a/public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/TeamPicker.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap diff --git a/public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap b/public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap similarity index 100% rename from public/app/core/components/Picker/__snapshots__/UserPicker.jest.tsx.snap rename to public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap diff --git a/public/app/core/components/Picker/withPicker.tsx b/public/app/core/components/Picker/withPicker.tsx deleted file mode 100644 index 838ef927c30..00000000000 --- a/public/app/core/components/Picker/withPicker.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { Component } from 'react'; - -export interface IProps { - backendSrv: any; - handlePicked: (data) => void; - value?: string; - className?: string; -} - -export default function withPicker(WrappedComponent) { - return class WithPicker extends Component { - constructor(props) { - super(props); - this.toggleLoading = this.toggleLoading.bind(this); - - this.state = { - isLoading: false, - }; - } - - toggleLoading(isLoading) { - this.setState(prevState => { - return { - ...prevState, - isLoading: isLoading, - }; - }); - } - - render() { - return ; - } - }; -} diff --git a/public/app/core/components/TagFilter/TagBadge.tsx b/public/app/core/components/TagFilter/TagBadge.tsx index e5c2e357a58..d93b5fd1e74 100644 --- a/public/app/core/components/TagFilter/TagBadge.tsx +++ b/public/app/core/components/TagFilter/TagBadge.tsx @@ -1,14 +1,14 @@ import React from 'react'; import tags from 'app/core/utils/tags'; -export interface IProps { +export interface Props { label: string; removeIcon: boolean; count: number; onClick: any; } -export class TagBadge extends React.Component { +export class TagBadge extends React.Component { constructor(props) { super(props); this.onClick = this.onClick.bind(this); diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 0b6058f3dd2..a879f544da0 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -4,13 +4,13 @@ import { Async } from 'react-select'; import { TagValue } from './TagValue'; import { TagOption } from './TagOption'; -export interface IProps { +export interface Props { tags: string[]; tagOptions: () => any; onSelect: (tag: string) => void; } -export class TagFilter extends React.Component { +export class TagFilter extends React.Component { inlineTags: boolean; constructor(props) { @@ -43,7 +43,7 @@ export class TagFilter extends React.Component { } render() { - let selectOptions = { + const selectOptions = { loadOptions: this.searchTags, onChange: this.onChange, value: this.props.tags, diff --git a/public/app/core/components/TagFilter/TagOption.tsx b/public/app/core/components/TagFilter/TagOption.tsx index 402544dd5f3..5938c98f870 100644 --- a/public/app/core/components/TagFilter/TagOption.tsx +++ b/public/app/core/components/TagFilter/TagOption.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { TagBadge } from './TagBadge'; -export interface IProps { +export interface Props { onSelect: any; onFocus: any; option: any; @@ -9,7 +9,7 @@ export interface IProps { className: any; } -export class TagOption extends React.Component { +export class TagOption extends React.Component { constructor(props) { super(props); this.handleMouseDown = this.handleMouseDown.bind(this); diff --git a/public/app/core/components/TagFilter/TagValue.tsx b/public/app/core/components/TagFilter/TagValue.tsx index 2e7819951f2..ca8ca9e4fba 100644 --- a/public/app/core/components/TagFilter/TagValue.tsx +++ b/public/app/core/components/TagFilter/TagValue.tsx @@ -1,14 +1,14 @@ import React from 'react'; import { TagBadge } from './TagBadge'; -export interface IProps { +export interface Props { value: any; className: any; onClick: any; onRemove: any; } -export class TagValue extends React.Component { +export class TagValue extends React.Component { constructor(props) { super(props); this.onClick = this.onClick.bind(this); diff --git a/public/app/core/components/Tooltip/Popover.jest.tsx b/public/app/core/components/Tooltip/Popover.test.tsx similarity index 100% rename from public/app/core/components/Tooltip/Popover.jest.tsx rename to public/app/core/components/Tooltip/Popover.test.tsx diff --git a/public/app/core/components/Tooltip/Popover.tsx b/public/app/core/components/Tooltip/Popover.tsx index 4dc25d34130..ee86d07fb53 100644 --- a/public/app/core/components/Tooltip/Popover.tsx +++ b/public/app/core/components/Tooltip/Popover.tsx @@ -2,11 +2,11 @@ import withTooltip from './withTooltip'; import { Target } from 'react-popper'; -interface IPopoverProps { +interface PopoverProps { tooltipSetState: (prevState: object) => void; } -class Popover extends React.Component { +class Popover extends React.Component { constructor(props) { super(props); this.toggleTooltip = this.toggleTooltip.bind(this); diff --git a/public/app/core/components/Tooltip/Tooltip.jest.tsx b/public/app/core/components/Tooltip/Tooltip.test.tsx similarity index 100% rename from public/app/core/components/Tooltip/Tooltip.jest.tsx rename to public/app/core/components/Tooltip/Tooltip.test.tsx diff --git a/public/app/core/components/Tooltip/Tooltip.tsx b/public/app/core/components/Tooltip/Tooltip.tsx index ae4093ea3f1..a265c8487d3 100644 --- a/public/app/core/components/Tooltip/Tooltip.tsx +++ b/public/app/core/components/Tooltip/Tooltip.tsx @@ -2,11 +2,11 @@ import withTooltip from './withTooltip'; import { Target } from 'react-popper'; -interface ITooltipProps { +interface TooltipProps { tooltipSetState: (prevState: object) => void; } -class Tooltip extends React.Component { +class Tooltip extends React.Component { constructor(props) { super(props); this.showTooltip = this.showTooltip.bind(this); diff --git a/public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap similarity index 100% rename from public/app/core/components/Tooltip/__snapshots__/Popover.jest.tsx.snap rename to public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap diff --git a/public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap b/public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap similarity index 100% rename from public/app/core/components/Tooltip/__snapshots__/Tooltip.jest.tsx.snap rename to public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap diff --git a/public/app/core/components/code_editor/code_editor.ts b/public/app/core/components/code_editor/code_editor.ts index 886ae2a6407..66aec778d73 100644 --- a/public/app/core/components/code_editor/code_editor.ts +++ b/public/app/core/components/code_editor/code_editor.ts @@ -53,23 +53,23 @@ const DEFAULT_TAB_SIZE = 2; const DEFAULT_BEHAVIOURS = true; const DEFAULT_SNIPPETS = true; -let editorTemplate = `
    `; +const editorTemplate = `
    `; function link(scope, elem, attrs) { // Options - let langMode = attrs.mode || DEFAULT_MODE; - let maxLines = attrs.maxLines || DEFAULT_MAX_LINES; - let showGutter = attrs.showGutter !== undefined; - let tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; - let behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; - let snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; + const langMode = attrs.mode || DEFAULT_MODE; + const maxLines = attrs.maxLines || DEFAULT_MAX_LINES; + const showGutter = attrs.showGutter !== undefined; + const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE; + const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS; + const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS; // Initialize editor - let aceElem = elem.get(0); - let codeEditor = ace.edit(aceElem); - let editorSession = codeEditor.getSession(); + const aceElem = elem.get(0); + const codeEditor = ace.edit(aceElem); + const editorSession = codeEditor.getSession(); - let editorOptions = { + const editorOptions = { maxLines: maxLines, showGutter: showGutter, tabSize: tabSize, @@ -93,7 +93,7 @@ function link(scope, elem, attrs) { // Add classes elem.addClass('gf-code-editor'); - let textarea = elem.find('textarea'); + const textarea = elem.find('textarea'); textarea.addClass('gf-form-input'); if (scope.codeEditorFocus) { @@ -110,14 +110,14 @@ function link(scope, elem, attrs) { // Event handlers editorSession.on('change', e => { scope.$apply(() => { - let newValue = codeEditor.getValue(); + const newValue = codeEditor.getValue(); scope.content = newValue; }); }); // Sync with outer scope - update editor content if model has been changed from outside of directive. scope.$watch('content', (newValue, oldValue) => { - let editorValue = codeEditor.getValue(); + const editorValue = codeEditor.getValue(); if (newValue !== editorValue && newValue !== oldValue) { scope.$$postDigest(function() { setEditorContent(newValue); @@ -157,7 +157,7 @@ function link(scope, elem, attrs) { anyEditor.completers.push(scope.getCompleter()); } - let aceModeName = `ace/mode/${lang}`; + const aceModeName = `ace/mode/${lang}`; editorSession.setMode(aceModeName); } diff --git a/public/app/core/components/colorpicker/ColorPalette.tsx b/public/app/core/components/colorpicker/ColorPalette.tsx index 07b25a32046..edb2629d16d 100644 --- a/public/app/core/components/colorpicker/ColorPalette.tsx +++ b/public/app/core/components/colorpicker/ColorPalette.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { sortedColors } from 'app/core/utils/colors'; -export interface IProps { +export interface Props { color: string; onColorSelect: (c: string) => void; } -export class ColorPalette extends React.Component { +export class ColorPalette extends React.Component { paletteColors: string[]; constructor(props) { @@ -29,7 +29,8 @@ export class ColorPalette extends React.Component { key={paletteColor} className={'pointer fa ' + cssClass} style={{ color: paletteColor }} - onClick={this.onColorSelect(paletteColor)}> + onClick={this.onColorSelect(paletteColor)} + >   ); @@ -41,4 +42,3 @@ export class ColorPalette extends React.Component { ); } } - diff --git a/public/app/core/components/colorpicker/ColorPicker.tsx b/public/app/core/components/colorpicker/ColorPicker.tsx index dbba75636d0..6e5083b6d6b 100644 --- a/public/app/core/components/colorpicker/ColorPicker.tsx +++ b/public/app/core/components/colorpicker/ColorPicker.tsx @@ -5,12 +5,12 @@ import Drop from 'tether-drop'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface IProps { +export interface Props { color: string; onChange: (c: string) => void; } -export class ColorPicker extends React.Component { +export class ColorPicker extends React.Component { pickerElem: any; colorPickerDrop: any; @@ -29,10 +29,10 @@ export class ColorPicker extends React.Component { openColorPicker() { const dropContent = ; - let dropContentElem = document.createElement('div'); + const dropContentElem = document.createElement('div'); ReactDOM.render(dropContent, dropContentElem); - let drop = new Drop({ + const drop = new Drop({ target: this.pickerElem[0], content: dropContentElem, position: 'top center', diff --git a/public/app/core/components/colorpicker/ColorPickerPopover.tsx b/public/app/core/components/colorpicker/ColorPickerPopover.tsx index 360c3fdd5c4..c42bcfa1d06 100644 --- a/public/app/core/components/colorpicker/ColorPickerPopover.tsx +++ b/public/app/core/components/colorpicker/ColorPickerPopover.tsx @@ -6,12 +6,12 @@ import { SpectrumPicker } from './SpectrumPicker'; const DEFAULT_COLOR = '#000000'; -export interface IProps { +export interface Props { color: string; onColorSelect: (c: string) => void; } -export class ColorPickerPopover extends React.Component { +export class ColorPickerPopover extends React.Component { pickerNavElem: any; constructor(props) { @@ -19,7 +19,7 @@ export class ColorPickerPopover extends React.Component { this.state = { tab: 'palette', color: this.props.color || DEFAULT_COLOR, - colorString: this.props.color || DEFAULT_COLOR + colorString: this.props.color || DEFAULT_COLOR, }; } @@ -28,11 +28,11 @@ export class ColorPickerPopover extends React.Component { } setColor(color) { - let newColor = tinycolor(color); + const newColor = tinycolor(color); if (newColor.isValid()) { this.setState({ color: newColor.toString(), - colorString: newColor.toString() + colorString: newColor.toString(), }); this.props.onColorSelect(color); } @@ -43,20 +43,20 @@ export class ColorPickerPopover extends React.Component { } spectrumColorSelected(color) { - let rgbColor = color.toRgbString(); + const rgbColor = color.toRgbString(); this.setColor(rgbColor); } onColorStringChange(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setState({ - colorString: colorString + colorString: colorString, }); - let newColor = tinycolor(colorString); + const newColor = tinycolor(colorString); if (newColor.isValid()) { // Update only color state - let newColorString = newColor.toString(); + const newColorString = newColor.toString(); this.setState({ color: newColorString, }); @@ -65,17 +65,17 @@ export class ColorPickerPopover extends React.Component { } onColorStringBlur(e) { - let colorString = e.target.value; + const colorString = e.target.value; this.setColor(colorString); } componentDidMount() { this.pickerNavElem.find('li:first').addClass('active'); - this.pickerNavElem.on('show', (e) => { + this.pickerNavElem.on('show', e => { // use href attr (#name => name) - let tab = e.target.hash.slice(1); + const tab = e.target.hash.slice(1); this.setState({ - tab: tab + tab: tab, }); }); } @@ -97,19 +97,24 @@ export class ColorPickerPopover extends React.Component {
    -
    - {currentTab} -
    +
    {currentTab}
    - - +
    ); diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index 3b24b9a4661..b514899e2e2 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { ColorPickerPopover } from './ColorPickerPopover'; import { react2AngularDirective } from 'app/core/utils/react2angular'; -export interface IProps { +export interface Props { series: any; onColorChange: (color: string) => void; onToggleAxis: () => void; } -export class SeriesColorPicker extends React.Component { +export class SeriesColorPicker extends React.Component { constructor(props) { super(props); this.onColorChange = this.onColorChange.bind(this); diff --git a/public/app/core/components/colorpicker/SpectrumPicker.tsx b/public/app/core/components/colorpicker/SpectrumPicker.tsx index eef04545308..15a76068e9b 100644 --- a/public/app/core/components/colorpicker/SpectrumPicker.tsx +++ b/public/app/core/components/colorpicker/SpectrumPicker.tsx @@ -3,13 +3,13 @@ import _ from 'lodash'; import $ from 'jquery'; import 'vendor/spectrum'; -export interface IProps { +export interface Props { color: string; options: object; onColorSelect: (c: string) => void; } -export class SpectrumPicker extends React.Component { +export class SpectrumPicker extends React.Component { elem: any; isMoving: boolean; @@ -29,14 +29,17 @@ export class SpectrumPicker extends React.Component { } componentDidMount() { - let spectrumOptions = _.assignIn({ - flat: true, - showAlpha: true, - showButtons: false, - color: this.props.color, - appendTo: this.elem, - move: this.onSpectrumMove, - }, this.props.options); + const spectrumOptions = _.assignIn( + { + flat: true, + showAlpha: true, + showButtons: false, + color: this.props.color, + appendTo: this.elem, + move: this.onSpectrumMove, + }, + this.props.options + ); this.elem.spectrum(spectrumOptions); this.elem.spectrum('show'); @@ -64,9 +67,6 @@ export class SpectrumPicker extends React.Component { } render() { - return ( -
    - ); + return
    ; } } - diff --git a/public/app/core/components/form_dropdown/form_dropdown.ts b/public/app/core/components/form_dropdown/form_dropdown.ts index 7ac55e54cf1..4604e1d7838 100644 --- a/public/app/core/components/form_dropdown/form_dropdown.ts +++ b/public/app/core/components/form_dropdown/form_dropdown.ts @@ -67,7 +67,7 @@ export class FormDropdownCtrl { // modify typeahead lookup // this = typeahead - var typeahead = this.inputElement.data('typeahead'); + const typeahead = this.inputElement.data('typeahead'); typeahead.lookup = function() { this.query = this.$element.val() || ''; this.source(this.query, this.process.bind(this)); @@ -100,7 +100,7 @@ export class FormDropdownCtrl { } getOptionsInternal(query) { - var result = this.getOptions({ $query: query }); + const result = this.getOptions({ $query: query }); if (this.isPromiseLike(result)) { return result; } @@ -118,7 +118,7 @@ export class FormDropdownCtrl { // if we have text use it if (this.lookupText) { this.getOptionsInternal('').then(options => { - var item = _.find(options, { value: this.model }); + const item = _.find(options, { value: this.model }); this.updateDisplay(item ? item.text : this.model); }); } else { @@ -132,7 +132,7 @@ export class FormDropdownCtrl { this.optionCache = options; // extract texts - let optionTexts = _.map(options, op => { + const optionTexts = _.map(options, op => { return _.escape(op.text); }); @@ -186,7 +186,7 @@ export class FormDropdownCtrl { } this.$scope.$apply(() => { - var option = _.find(this.optionCache, { text: text }); + const option = _.find(this.optionCache, { text: text }); if (option) { if (_.isObject(this.model)) { @@ -228,7 +228,7 @@ export class FormDropdownCtrl { this.linkElement.hide(); this.linkMode = false; - var typeahead = this.inputElement.data('typeahead'); + const typeahead = this.inputElement.data('typeahead'); if (typeahead) { this.inputElement.val(''); typeahead.lookup(); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index fa2c96ade32..c1cd0e2b5f2 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -8,7 +8,7 @@ import appEvents from 'app/core/app_events'; import Drop from 'tether-drop'; import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; -import { BackendSrv } from 'app/core/services/backend_srv'; +import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { configureStore } from 'app/store/configureStore'; @@ -25,7 +25,9 @@ export class GrafanaCtrl { backendSrv: BackendSrv, datasourceSrv: DatasourceSrv ) { + // sets singleston instances for angular services so react components can access them configureStore(); + setBackendSrv(backendSrv); createStore({ backendSrv, datasourceSrv }); $scope.init = function() { @@ -140,7 +142,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } // close all drops - for (let drop of Drop.drops) { + for (const drop of Drop.drops) { drop.destroy(); } }); diff --git a/public/app/core/components/help/help.ts b/public/app/core/components/help/help.ts index a1d3c34ae5b..eac47b6e0a2 100644 --- a/public/app/core/components/help/help.ts +++ b/public/app/core/components/help/help.ts @@ -25,6 +25,7 @@ export class HelpCtrl { { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' }, { keys: ['d', 'E'], description: 'Expand all rows' }, { keys: ['d', 'C'], description: 'Collapse all rows' }, + { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' }, { keys: ['mod+o'], description: 'Toggle shared graph crosshair' }, ], 'Focused Panel': [ diff --git a/public/app/core/components/info_popover.ts b/public/app/core/components/info_popover.ts index 59332a6f716..ae4feeec701 100644 --- a/public/app/core/components/info_popover.ts +++ b/public/app/core/components/info_popover.ts @@ -8,10 +8,10 @@ export function infoPopover() { template: '', transclude: true, link: function(scope, elem, attrs, ctrl, transclude) { - let offset = attrs.offset || '0 -10px'; - let position = attrs.position || 'right middle'; + const offset = attrs.offset || '0 -10px'; + const position = attrs.position || 'right middle'; let classes = 'drop-help drop-hide-out-of-bounds'; - let openOn = 'hover'; + const openOn = 'hover'; elem.addClass('gf-form-help-icon'); @@ -24,14 +24,14 @@ export function infoPopover() { } transclude(function(clone, newScope) { - let content = document.createElement('div'); + const content = document.createElement('div'); content.className = 'markdown-html'; _.each(clone, node => { content.appendChild(node); }); - let dropOptions = { + const dropOptions = { target: elem[0], content: content, position: position, @@ -52,9 +52,9 @@ export function infoPopover() { // Create drop in next digest after directive content is rendered. scope.$applyAsync(() => { - let drop = new Drop(dropOptions); + const drop = new Drop(dropOptions); - let unbind = scope.$on('$destroy', function() { + const unbind = scope.$on('$destroy', function() { drop.destroy(); unbind(); }); diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts index 86cd3066c48..0016305e617 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.ts +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -103,10 +103,10 @@ export class ManageDashboardsCtrl { this.sections = result; - for (let section of this.sections) { + for (const section of this.sections) { section.checked = false; - for (let dashboard of section.items) { + for (const dashboard of section.items) { dashboard.checked = false; } } @@ -119,7 +119,7 @@ export class ManageDashboardsCtrl { selectionChanged() { let selectedDashboards = 0; - for (let section of this.sections) { + for (const section of this.sections) { selectedDashboards += _.filter(section.items, { checked: true }).length; } @@ -129,7 +129,7 @@ export class ManageDashboardsCtrl { } getFoldersAndDashboardsToDelete() { - let selectedDashboards = { + const selectedDashboards = { folders: [], dashboards: [], }; @@ -148,7 +148,7 @@ export class ManageDashboardsCtrl { getFolderIds(sections) { const ids = []; - for (let s of sections) { + for (const s of sections) { if (s.checked) { ids.push(s.id); } @@ -191,7 +191,7 @@ export class ManageDashboardsCtrl { } getDashboardsToMove() { - let selectedDashboards = []; + const selectedDashboards = []; for (const section of this.sections) { const selected = _.filter(section.items, { checked: true }); @@ -238,7 +238,7 @@ export class ManageDashboardsCtrl { } onTagFilterChange() { - var res = this.filterByTag(this.selectedTagFilter.term); + const res = this.filterByTag(this.selectedTagFilter.term); this.selectedTagFilter = this.tagFilterOptions[0]; return res; } @@ -264,7 +264,7 @@ export class ManageDashboardsCtrl { } onSelectAllChanged() { - for (let section of this.sections) { + for (const section of this.sections) { if (!section.hideHeader) { section.checked = this.selectAllChecked; } diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts index e6db344a4d6..b6603f06175 100644 --- a/public/app/core/components/scroll/page_scroll.ts +++ b/public/app/core/components/scroll/page_scroll.ts @@ -29,11 +29,13 @@ export function pageScrollbar() { scope.$on('$routeChangeSuccess', () => { lastPos = 0; elem[0].scrollTop = 0; - elem[0].focus(); + // Focus page to enable scrolling by keyboard + elem[0].focus({ preventScroll: true }); }); elem[0].tabIndex = -1; - elem[0].focus(); + // Focus page to enable scrolling by keyboard + elem[0].focus({ preventScroll: true }); }, }; } diff --git a/public/app/core/components/scroll/scroll.ts b/public/app/core/components/scroll/scroll.ts index 3f9865e6dce..5cdbdb62ee3 100644 --- a/public/app/core/components/scroll/scroll.ts +++ b/public/app/core/components/scroll/scroll.ts @@ -17,7 +17,7 @@ export function geminiScrollbar() { restrict: 'A', link: function(scope, elem, attrs) { let scrollRoot = elem.parent(); - let scroller = elem; + const scroller = elem; if (attrs.grafanaScrollbar && attrs.grafanaScrollbar === 'scrollonroot') { scrollRoot = scroller; @@ -27,7 +27,7 @@ export function geminiScrollbar() { $(scrollBarHTML).appendTo(scrollRoot); elem.addClass(scrollerClass); - let scrollParams = { + const scrollParams = { root: scrollRoot[0], scroller: scroller[0], bar: '.baron__bar', @@ -37,7 +37,7 @@ export function geminiScrollbar() { direction: 'v', }; - let scrollbar = baron(scrollParams); + const scrollbar = baron(scrollParams); let lastPos = 0; diff --git a/public/app/core/components/search/SearchResult.tsx b/public/app/core/components/search/SearchResult.tsx index 5ab4bba8edb..3141d29ac7f 100644 --- a/public/app/core/components/search/SearchResult.tsx +++ b/public/app/core/components/search/SearchResult.tsx @@ -54,7 +54,7 @@ export class SearchResultSection extends React.Component { }; render() { - let collapseClassNames = classNames({ + const collapseClassNames = classNames({ fa: true, 'fa-plus': !this.props.section.expanded, 'fa-minus': this.props.section.expanded, diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index fb9d9be7f70..5649963c3dc 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -17,13 +17,13 @@ export class SideMenuCtrl { this.isSignedIn = contextSrv.isSignedIn; this.user = contextSrv.user; - let navTree = _.cloneDeep(config.bootData.navTree); + const navTree = _.cloneDeep(config.bootData.navTree); this.mainLinks = _.filter(navTree, item => !item.hideFromMenu); this.bottomNav = _.filter(navTree, item => item.hideFromMenu); this.loginUrl = 'login?redirect=' + encodeURIComponent(this.$location.path()); if (contextSrv.user.orgCount > 1) { - let profileNode = _.find(this.bottomNav, { id: 'profile' }); + const profileNode = _.find(this.bottomNav, { id: 'profile' }); if (profileNode) { profileNode.showOrgSwitcher = true; } diff --git a/public/app/core/components/team_picker.ts b/public/app/core/components/team_picker.ts deleted file mode 100644 index 228767a76c4..00000000000 --- a/public/app/core/components/team_picker.ts +++ /dev/null @@ -1,64 +0,0 @@ -import coreModule from 'app/core/core_module'; -import _ from 'lodash'; - -const template = ` - -`; -export class TeamPickerCtrl { - group: any; - teamPicked: any; - debouncedSearchGroups: any; - - /** @ngInject */ - constructor(private backendSrv) { - this.debouncedSearchGroups = _.debounce(this.searchGroups, 500, { - leading: true, - trailing: false, - }); - this.reset(); - } - - reset() { - this.group = { text: 'Choose', value: null }; - } - - searchGroups(query: string) { - return Promise.resolve( - this.backendSrv.get('/api/teams/search?perpage=10&page=1&query=' + query).then(result => { - return _.map(result.teams, ug => { - return { text: ug.name, value: ug }; - }); - }) - ); - } - - onChange(option) { - this.teamPicked({ $group: option.value }); - } -} - -export function teamPicker() { - return { - restrict: 'E', - template: template, - controller: TeamPickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - teamPicked: '&', - }, - link: function(scope, elem, attrs, ctrl) { - scope.$on('team-picker-reset', () => { - ctrl.reset(); - }); - }, - }; -} - -coreModule.directive('teamPicker', teamPicker); diff --git a/public/app/core/components/user_picker.ts b/public/app/core/components/user_picker.ts deleted file mode 100644 index 606ded09885..00000000000 --- a/public/app/core/components/user_picker.ts +++ /dev/null @@ -1,71 +0,0 @@ -import coreModule from 'app/core/core_module'; -import _ from 'lodash'; - -const template = ` - -`; -export class UserPickerCtrl { - user: any; - debouncedSearchUsers: any; - userPicked: any; - - /** @ngInject */ - constructor(private backendSrv) { - this.reset(); - this.debouncedSearchUsers = _.debounce(this.searchUsers, 500, { - leading: true, - trailing: false, - }); - } - - searchUsers(query: string) { - return Promise.resolve( - this.backendSrv.get('/api/users/search?perpage=10&page=1&query=' + query).then(result => { - return _.map(result.users, user => { - return { text: user.login + ' - ' + user.email, value: user }; - }); - }) - ); - } - - onChange(option) { - this.userPicked({ $user: option.value }); - } - - reset() { - this.user = { text: 'Choose', value: null }; - } -} - -export interface User { - id: number; - name: string; - login: string; - email: string; -} - -export function userPicker() { - return { - restrict: 'E', - template: template, - controller: UserPickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - userPicked: '&', - }, - link: function(scope, elem, attrs, ctrl) { - scope.$on('user-picker-reset', () => { - ctrl.reset(); - }); - }, - }; -} - -coreModule.directive('userPicker', userPicker); diff --git a/public/app/core/config.ts b/public/app/core/config.ts index e065ddb22fb..f522c6340e6 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -31,7 +31,7 @@ export class Settings { loginError: any; constructor(options) { - var defaults = { + const defaults = { datasources: {}, window_title_prefix: 'Grafana - ', panels: {}, @@ -51,8 +51,8 @@ export class Settings { } } -var bootData = (window).grafanaBootData || { settings: {} }; -var options = bootData.settings; +const bootData = (window).grafanaBootData || { settings: {} }; +const options = bootData.settings; options.bootData = bootData; const config = new Settings(options); diff --git a/public/app/core/controllers/inspect_ctrl.ts b/public/app/core/controllers/inspect_ctrl.ts index 5dd4cb3d06f..612b9cac42e 100644 --- a/public/app/core/controllers/inspect_ctrl.ts +++ b/public/app/core/controllers/inspect_ctrl.ts @@ -6,7 +6,7 @@ import coreModule from '../core_module'; export class InspectCtrl { /** @ngInject */ constructor($scope, $sanitize) { - var model = $scope.inspector; + const model = $scope.inspector; $scope.init = function() { $scope.editor = { index: 0 }; @@ -53,10 +53,10 @@ export class InspectCtrl { }; } getParametersFromQueryString(queryString) { - var result = []; - var parameters = queryString.split('&'); - for (var i = 0; i < parameters.length; i++) { - var keyValue = parameters[i].split('='); + const result = []; + const parameters = queryString.split('&'); + for (let i = 0; i < parameters.length; i++) { + const keyValue = parameters[i].split('='); if (keyValue[1].length > 0) { result.push({ key: keyValue[0], diff --git a/public/app/core/controllers/json_editor_ctrl.ts b/public/app/core/controllers/json_editor_ctrl.ts index d369fe8b3c0..3260f6ff537 100644 --- a/public/app/core/controllers/json_editor_ctrl.ts +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -9,7 +9,7 @@ export class JsonEditorCtrl { $scope.canCopy = $scope.enableCopy; $scope.update = function() { - var newObject = angular.fromJson($scope.json); + const newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; diff --git a/public/app/core/controllers/login_ctrl.ts b/public/app/core/controllers/login_ctrl.ts index 0a66f83d08a..daf562c65da 100644 --- a/public/app/core/controllers/login_ctrl.ts +++ b/public/app/core/controllers/login_ctrl.ts @@ -45,8 +45,8 @@ export class LoginCtrl { }; $scope.changeView = function() { - let loginView = document.querySelector('#login-view'); - let changePasswordView = document.querySelector('#change-password-view'); + const loginView = document.querySelector('#login-view'); + const changePasswordView = document.querySelector('#change-password-view'); loginView.className += ' add'; setTimeout(() => { @@ -118,7 +118,7 @@ export class LoginCtrl { }; $scope.toGrafana = function() { - var params = $location.search(); + const params = $location.search(); if (params.redirect && params.redirect[0] === '/') { window.location.href = config.appSubUrl + params.redirect; diff --git a/public/app/core/controllers/reset_password_ctrl.ts b/public/app/core/controllers/reset_password_ctrl.ts index 360c5bf8071..244f0307150 100644 --- a/public/app/core/controllers/reset_password_ctrl.ts +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -7,7 +7,7 @@ export class ResetPasswordCtrl { $scope.formModel = {}; $scope.mode = 'send'; - var params = $location.search(); + const params = $location.search(); if (params.code) { $scope.mode = 'reset'; $scope.formModel.code = params.code; diff --git a/public/app/core/controllers/signup_ctrl.ts b/public/app/core/controllers/signup_ctrl.ts index ad23e0f7f22..5a85e1605b7 100644 --- a/public/app/core/controllers/signup_ctrl.ts +++ b/public/app/core/controllers/signup_ctrl.ts @@ -9,7 +9,7 @@ export class SignUpCtrl { $scope.formModel = {}; - var params = $location.search(); + const params = $location.search(); // validate email is semi ok if (params.email && !params.email.match(/^\S+@\S+$/)) { diff --git a/public/app/core/core.ts b/public/app/core/core.ts index fb7021fe883..d6088283f3b 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -44,8 +44,6 @@ 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 { userPicker } from './components/user_picker'; -import { teamPicker } from './components/team_picker'; import { geminiScrollbar } from './components/scroll/scroll'; import { pageScrollbar } from './components/scroll/page_scroll'; import { gfPageDirective } from './components/gf_page'; @@ -83,8 +81,6 @@ export { JsonExplorer, NavModelSrv, NavModel, - userPicker, - teamPicker, geminiScrollbar, pageScrollbar, gfPageDirective, diff --git a/public/app/core/directives/dropdown_typeahead.ts b/public/app/core/directives/dropdown_typeahead.ts index c9e44c5e786..af8c4ddc3bb 100644 --- a/public/app/core/directives/dropdown_typeahead.ts +++ b/public/app/core/directives/dropdown_typeahead.ts @@ -4,12 +4,12 @@ import coreModule from '../core_module'; /** @ngInject */ export function dropdownTypeahead($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -21,8 +21,8 @@ export function dropdownTypeahead($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -42,7 +42,7 @@ export function dropdownTypeahead($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -60,8 +60,8 @@ export function dropdownTypeahead($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -74,7 +74,7 @@ export function dropdownTypeahead($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { @@ -124,10 +124,10 @@ export function dropdownTypeahead($compile) { /** @ngInject */ export function dropdownTypeahead2($compile) { - let inputTemplate = + const inputTemplate = ''; - let buttonTemplate = + const buttonTemplate = ''; @@ -139,8 +139,8 @@ export function dropdownTypeahead2($compile) { model: '=ngModel', }, link: function($scope, elem, attrs) { - let $input = $(inputTemplate); - let $button = $(buttonTemplate); + const $input = $(inputTemplate); + const $button = $(buttonTemplate); $input.appendTo(elem); $button.appendTo(elem); @@ -160,7 +160,7 @@ export function dropdownTypeahead2($compile) { }); } - let typeaheadValues = _.reduce( + const typeaheadValues = _.reduce( $scope.menuItems, function(memo, value, index) { if (!value.submenu) { @@ -178,8 +178,8 @@ export function dropdownTypeahead2($compile) { ); $scope.menuItemSelected = function(index, subIndex) { - let menuItem = $scope.menuItems[index]; - let payload: any = { $item: menuItem }; + const menuItem = $scope.menuItems[index]; + const payload: any = { $item: menuItem }; if (menuItem.submenu && subIndex !== void 0) { payload.$subItem = menuItem.submenu[subIndex]; } @@ -192,7 +192,7 @@ export function dropdownTypeahead2($compile) { minLength: 1, items: 10, updater: function(value) { - let result: any = {}; + const result: any = {}; _.each($scope.menuItems, function(menuItem) { _.each(menuItem.submenu, function(submenuItem) { if (value === menuItem.text + ' ' + submenuItem.text) { diff --git a/public/app/core/directives/metric_segment.ts b/public/app/core/directives/metric_segment.ts index 3718d7fbd4a..117f776f487 100644 --- a/public/app/core/directives/metric_segment.ts +++ b/public/app/core/directives/metric_segment.ts @@ -4,16 +4,16 @@ import coreModule from '../core_module'; /** @ngInject */ export function metricSegment($compile, $sce) { - let inputTemplate = + const inputTemplate = ''; - let linkTemplate = + const linkTemplate = ''; - let selectTemplate = + const selectTemplate = ''; @@ -25,13 +25,13 @@ export function metricSegment($compile, $sce) { debounce: '@', }, link: function($scope, elem) { - let $input = $(inputTemplate); - let segment = $scope.segment; - let $button = $(segment.selectMode ? selectTemplate : linkTemplate); + const $input = $(inputTemplate); + const segment = $scope.segment; + const $button = $(segment.selectMode ? selectTemplate : linkTemplate); let options = null; let cancelBlur = null; let linkMode = true; - let debounceLookup = $scope.debounce; + const debounceLookup = $scope.debounce; $input.appendTo(elem); $button.appendTo(elem); @@ -44,7 +44,7 @@ export function metricSegment($compile, $sce) { value = _.unescape(value); $scope.$apply(function() { - let selected = _.find($scope.altSegments, { value: value }); + const selected = _.find($scope.altSegments, { value: value }); if (selected) { segment.value = selected.value; segment.html = selected.html || selected.value; @@ -141,10 +141,10 @@ export function metricSegment($compile, $sce) { matcher: $scope.matcher, }); - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); typeahead.lookup = function() { this.query = this.$element.val() || ''; - let items = this.source(this.query, $.proxy(this.process, this)); + const items = this.source(this.query, $.proxy(this.process, this)); return items ? this.process(items) : items; }; @@ -169,7 +169,7 @@ export function metricSegment($compile, $sce) { linkMode = false; - let typeahead = $input.data('typeahead'); + const typeahead = $input.data('typeahead'); if (typeahead) { $input.val(''); typeahead.lookup(); @@ -200,8 +200,8 @@ export function metricSegmentModel(uiSegmentSrv, $q) { let cachedOptions; $scope.valueToSegment = function(value) { - let option = _.find($scope.options, { value: value }); - let segment = { + const option = _.find($scope.options, { value: value }); + const segment = { cssClass: attrs.cssClass, custom: attrs.custom, value: option ? option.text : value, @@ -234,7 +234,7 @@ export function metricSegmentModel(uiSegmentSrv, $q) { $scope.onSegmentChange = function() { if (cachedOptions) { - let option = _.find(cachedOptions, { text: $scope.segment.value }); + const option = _.find(cachedOptions, { text: $scope.segment.value }); if (option && option.value !== $scope.property) { $scope.property = option.value; } else if (attrs.custom !== 'false') { diff --git a/public/app/core/directives/misc.ts b/public/app/core/directives/misc.ts index 299de05f112..034b312aa0e 100644 --- a/public/app/core/directives/misc.ts +++ b/public/app/core/directives/misc.ts @@ -156,7 +156,7 @@ function gfDropdown($parse, $compile, $timeout) { var ul = ['']; for (let index = 0; index < items.length; index++) { - let item = items[index]; + const item = items[index]; if (item.divider) { ul.splice(index + 1, 0, '
  • '); diff --git a/public/app/core/directives/value_select_dropdown.ts b/public/app/core/directives/value_select_dropdown.ts index d384904c2d8..69504c1bb1b 100644 --- a/public/app/core/directives/value_select_dropdown.ts +++ b/public/app/core/directives/value_select_dropdown.ts @@ -46,16 +46,16 @@ export class ValueSelectDropdownCtrl { } updateLinkText() { - let current = this.variable.current; + const current = this.variable.current; if (current.tags && current.tags.length) { // filer out values that are in selected tags - let selectedAndNotInTag = _.filter(this.variable.options, option => { + const selectedAndNotInTag = _.filter(this.variable.options, option => { if (!option.selected) { return false; } for (let i = 0; i < current.tags.length; i++) { - let tag = current.tags[i]; + const tag = current.tags[i]; if (_.indexOf(tag.values, option.value) !== -1) { return false; } @@ -64,7 +64,7 @@ export class ValueSelectDropdownCtrl { }); // convert values to text - let currentTexts = _.map(selectedAndNotInTag, 'text'); + const currentTexts = _.map(selectedAndNotInTag, 'text'); // join texts this.linkText = currentTexts.join(' + '); @@ -142,7 +142,7 @@ export class ValueSelectDropdownCtrl { commitChange = commitChange || false; excludeOthers = excludeOthers || false; - let setAllExceptCurrentTo = newValue => { + const setAllExceptCurrentTo = newValue => { _.each(this.options, other => { if (option !== other) { other.selected = newValue; @@ -246,9 +246,9 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { controllerAs: 'vm', bindToController: true, link: function(scope, elem) { - let bodyEl = angular.element($window.document.body); - let linkEl = elem.find('.variable-value-link'); - let inputEl = elem.find('input'); + const bodyEl = angular.element($window.document.body); + const linkEl = elem.find('.variable-value-link'); + const inputEl = elem.find('input'); function openDropdown() { inputEl.css('width', Math.max(linkEl.width(), 80) + 'px'); @@ -288,7 +288,7 @@ export function valueSelectDropdown($compile, $window, $timeout, $rootScope) { } }); - let cleanUp = $rootScope.$on('template-variable-value-updated', () => { + const cleanUp = $rootScope.$on('template-variable-value-updated', () => { scope.vm.updateLinkText(); }); diff --git a/public/app/core/filters/filters.ts b/public/app/core/filters/filters.ts index c6aea32d38d..098bda58de9 100644 --- a/public/app/core/filters/filters.ts +++ b/public/app/core/filters/filters.ts @@ -38,7 +38,7 @@ coreModule.filter('moment', function() { }); coreModule.filter('noXml', function() { - var noXml = function(text) { + const noXml = function(text) { return _.isString(text) ? text .replace(/&/g, '&') @@ -55,7 +55,7 @@ coreModule.filter('noXml', function() { /** @ngInject */ function interpolateTemplateVars(templateSrv) { - var filterFunc: any = function(text, scope) { + const filterFunc: any = function(text, scope) { var scopedVars; if (scope.ctrl) { scopedVars = (scope.ctrl.panel || scope.ctrl.row).scopedVars; diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts new file mode 100644 index 00000000000..46e95a471ce --- /dev/null +++ b/public/app/core/logs_model.ts @@ -0,0 +1,29 @@ +export enum LogLevel { + crit = 'crit', + warn = 'warn', + err = 'error', + error = 'error', + info = 'info', + debug = 'debug', + trace = 'trace', +} + +export interface LogSearchMatch { + start: number; + length: number; + text?: string; +} + +export interface LogRow { + key: string; + entry: string; + logLevel: LogLevel; + timestamp: string; + timeFromNow: string; + timeLocal: string; + searchMatches?: LogSearchMatch[]; +} + +export interface LogsModel { + rows: LogRow[]; +} diff --git a/public/app/core/nav_model_srv.ts b/public/app/core/nav_model_srv.ts index a9ebd4e79ed..2bed33e70da 100644 --- a/public/app/core/nav_model_srv.ts +++ b/public/app/core/nav_model_srv.ts @@ -41,14 +41,14 @@ export class NavModelSrv { var children = this.navItems; var nav = new NavModel(); - for (let id of args) { + for (const id of args) { // if its a number then it's the index to use for main if (_.isNumber(id)) { nav.main = nav.breadcrumbs[id]; break; } - let node = _.find(children, { id: id }); + const node = _.find(children, { id: id }); nav.breadcrumbs.push(node); nav.node = node; nav.main = node; @@ -56,7 +56,7 @@ export class NavModelSrv { } if (nav.main.children) { - for (let item of nav.main.children) { + for (const item of nav.main.children) { item.active = false; if (item.url === nav.node.url) { diff --git a/public/app/core/services/alert_srv.ts b/public/app/core/services/alert_srv.ts index fc76ef9e371..19ad81667d7 100644 --- a/public/app/core/services/alert_srv.ts +++ b/public/app/core/services/alert_srv.ts @@ -60,14 +60,14 @@ export class AlertSrv { } } - var newAlert = { + const newAlert = { title: title || '', text: text || '', severity: severity || 'info', icon: this.getIconForSeverity(severity), }; - var newAlertJson = angular.toJson(newAlert); + const newAlertJson = angular.toJson(newAlert); // remove same alert if it already exists _.remove(this.list, function(value) { diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index d1998f44cbc..c3936f45451 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -12,7 +12,7 @@ export class Analytics { dataType: 'script', cache: true, }); - var ga = ((window).ga = + const ga = ((window).ga = (window).ga || function() { (ga.q = ga.q || []).push(arguments); @@ -25,8 +25,8 @@ export class Analytics { init() { this.$rootScope.$on('$viewContentLoaded', () => { - var track = { page: this.$location.url() }; - var ga = (window).ga || this.gaInit(); + const track = { page: this.$location.url() }; + const ga = (window).ga || this.gaInit(); ga('set', track); ga('send', 'pageview'); }); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index d582b6a3b18..4dd8a123378 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -276,11 +276,11 @@ export class BackendSrv { deleteFoldersAndDashboards(folderUids, dashboardUids) { const tasks = []; - for (let folderUid of folderUids) { + for (const folderUid of folderUids) { tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true)); } - for (let dashboardUid of dashboardUids) { + for (const dashboardUid of dashboardUids) { tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true)); } @@ -290,7 +290,7 @@ export class BackendSrv { moveDashboards(dashboardUids, toFolder) { const tasks = []; - for (let uid of dashboardUids) { + for (const uid of dashboardUids) { tasks.push(this.createTask(this.moveDashboard.bind(this), true, uid, toFolder)); } @@ -304,7 +304,7 @@ export class BackendSrv { } private moveDashboard(uid, toFolder) { - let deferred = this.$q.defer(); + const deferred = this.$q.defer(); this.getDashboardByUid(uid).then(fullDash => { const model = new DashboardModel(fullDash.dashboard, fullDash.meta); @@ -315,7 +315,7 @@ export class BackendSrv { } const clone = model.getSaveModelClone(); - let options = { + const options = { folderId: toFolder.id, overwrite: false, }; @@ -368,3 +368,17 @@ export class BackendSrv { } coreModule.service('backendSrv', BackendSrv); + +// +// Code below is to expore the service to react components +// + +let singletonInstance: BackendSrv; + +export function setBackendSrv(instance: BackendSrv) { + singletonInstance = instance; +} + +export function getBackendSrv(): BackendSrv { + return singletonInstance; +} diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 4a5649a6c52..bdc2976a94c 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -15,7 +15,7 @@ export class BridgeSrv { init() { this.$rootScope.$on('$routeUpdate', (evt, data) => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); if (store.view.currentUrl !== angularUrl) { store.view.updatePathAndQuery(this.$location.path(), this.$location.search(), this.$route.current.params); } @@ -28,7 +28,7 @@ export class BridgeSrv { reaction( () => store.view.currentUrl, currentUrl => { - let angularUrl = this.$location.url(); + const angularUrl = this.$location.url(); const url = locationUtil.stripBaseFromUrl(currentUrl); if (angularUrl !== url) { this.$timeout(() => { diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 086843b6f9a..ccd86856755 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -6,7 +6,7 @@ class DynamicDirectiveSrv { constructor(private $compile, private $rootScope) {} addDirective(element, name, scope) { - var child = angular.element(document.createElement(name)); + const child = angular.element(document.createElement(name)); this.$compile(child)(scope); element.empty(); @@ -36,7 +36,7 @@ class DynamicDirectiveSrv { } create(options) { - let directiveDef = { + const directiveDef = { restrict: 'E', scope: options.scope, link: (scope, elem, attrs) => { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index cbc7871fbbd..cad538d13aa 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -70,7 +70,7 @@ export class KeybindingSrv { } exit() { - var popups = $('.popover.in'); + const popups = $('.popover.in'); if (popups.length > 0) { return; } @@ -89,7 +89,7 @@ export class KeybindingSrv { } // close settings view - var search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; this.$location.search(search); @@ -123,7 +123,7 @@ export class KeybindingSrv { } showDashEditView() { - var search = _.extend(this.$location.search(), { editview: 'settings' }); + const search = _.extend(this.$location.search(), { editview: 'settings' }); this.$location.search(search); } @@ -191,7 +191,7 @@ export class KeybindingSrv { range, }; const exploreState = encodePathComponent(JSON.stringify(state)); - this.$location.url(`/explore/${exploreState}`); + this.$location.url(`/explore?state=${exploreState}`); } } }); @@ -210,7 +210,7 @@ export class KeybindingSrv { // duplicate panel this.bind('p d', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - let panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; + const panelIndex = dashboard.getPanelInfoById(dashboard.meta.focusPanelId).index; dashboard.duplicatePanel(dashboard.panels[panelIndex]); } }); @@ -218,8 +218,8 @@ export class KeybindingSrv { // share panel this.bind('p s', () => { if (dashboard.meta.focusPanelId) { - var shareScope = scope.$new(); - var panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); + const shareScope = scope.$new(); + const panelInfo = dashboard.getPanelInfoById(dashboard.meta.focusPanelId); shareScope.panel = panelInfo.panel; shareScope.dashboard = dashboard; @@ -259,6 +259,12 @@ export class KeybindingSrv { this.bind('d v', () => { appEvents.emit('toggle-view-mode'); }); + + //Autofit panels + this.bind('d a', () => { + // this has to be a full page reload + window.location.href = window.location.href + '&autofitpanels'; + }); } } diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts index 3c61412669e..aeffaaa9b3b 100644 --- a/public/app/core/services/ng_react.ts +++ b/public/app/core/services/ng_react.ts @@ -295,6 +295,6 @@ var reactDirective = function($injector) { }; }; -let ngModule = angular.module('react', []); +const ngModule = angular.module('react', []); ngModule.directive('reactComponent', ['$injector', reactComponent]); ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts index 113e1e5fae7..33568cda8d3 100644 --- a/public/app/core/services/popover_srv.ts +++ b/public/app/core/services/popover_srv.ts @@ -18,10 +18,10 @@ function popoverSrv($compile, $rootScope, $timeout) { openDrop = null; } - var scope = _.extend($rootScope.$new(true), options.model); - var drop; + const scope = _.extend($rootScope.$new(true), options.model); + let drop; - var cleanUp = () => { + const cleanUp = () => { setTimeout(() => { scope.$destroy(); @@ -41,7 +41,7 @@ function popoverSrv($compile, $rootScope, $timeout) { drop.close(); }; - var contentElement = document.createElement('div'); + const contentElement = document.createElement('div'); contentElement.innerHTML = options.template; $compile(contentElement)(scope); diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 9f32e21f3f6..017b2c15efc 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -85,10 +85,10 @@ export class SearchSrv { } search(options) { - let sections: any = {}; - let promises = []; - let query = _.clone(options); - let hasFilters = + const sections: any = {}; + const promises = []; + const query = _.clone(options); + const hasFilters = options.query || (options.tag && options.tag.length > 0) || options.starred || @@ -124,7 +124,7 @@ export class SearchSrv { } // create folder index - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { sections[hit.id] = { id: hit.id, @@ -140,7 +140,7 @@ export class SearchSrv { } } - for (let hit of results) { + for (const hit of results) { if (hit.type === 'dash-folder') { continue; } @@ -185,7 +185,7 @@ export class SearchSrv { return Promise.resolve(section); } - let query = { + const query = { folderIds: [section.id], }; diff --git a/public/app/core/services/segment_srv.ts b/public/app/core/services/segment_srv.ts index 042340e6102..5250febc11a 100644 --- a/public/app/core/services/segment_srv.ts +++ b/public/app/core/services/segment_srv.ts @@ -3,7 +3,7 @@ import coreModule from '../core_module'; /** @ngInject */ export function uiSegmentSrv($sce, templateSrv) { - let self = this; + const self = this; function MetricSegment(options) { if (options === '*' || options.value === '*') { @@ -78,7 +78,7 @@ export function uiSegmentSrv($sce, templateSrv) { this.transformToSegments = function(addTemplateVars, variableTypeFilter) { return function(results) { - let segments = _.map(results, function(segment) { + const segments = _.map(results, function(segment) { return self.newSegment({ value: segment.text, expandable: segment.expandable }); }); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts index 1afae0a02f4..da598fbb127 100644 --- a/public/app/core/services/util_srv.ts +++ b/public/app/core/services/util_srv.ts @@ -33,7 +33,7 @@ export class UtilSrv { this.modalScope = this.$rootScope.$new(); } - var modal = this.$modal({ + const modal = this.$modal({ modalClass: options.modalClass, template: options.src, templateHtml: options.templateHtml, @@ -50,7 +50,7 @@ export class UtilSrv { } showConfirmModal(payload) { - var scope = this.$rootScope.$new(); + const scope = this.$rootScope.$new(); scope.onConfirm = function() { payload.onConfirm(); diff --git a/public/app/core/specs/ColorPalette.jest.tsx b/public/app/core/specs/ColorPalette.test.tsx similarity index 100% rename from public/app/core/specs/ColorPalette.jest.tsx rename to public/app/core/specs/ColorPalette.test.tsx diff --git a/public/app/core/specs/PasswordStrength.jest.tsx b/public/app/core/specs/PasswordStrength.test.tsx similarity index 100% rename from public/app/core/specs/PasswordStrength.jest.tsx rename to public/app/core/specs/PasswordStrength.test.tsx diff --git a/public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap b/public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap similarity index 100% rename from public/app/core/specs/__snapshots__/ColorPalette.jest.tsx.snap rename to public/app/core/specs/__snapshots__/ColorPalette.test.tsx.snap diff --git a/public/app/core/specs/backend_srv.test.ts b/public/app/core/specs/backend_srv.test.ts new file mode 100644 index 00000000000..e9cd5973d36 --- /dev/null +++ b/public/app/core/specs/backend_srv.test.ts @@ -0,0 +1,25 @@ +import { BackendSrv } from 'app/core/services/backend_srv'; +jest.mock('app/core/store'); + +describe('backend_srv', function() { + const _httpBackend = options => { + if (options.url === 'gateway-error') { + return Promise.reject({ status: 502 }); + } + return Promise.resolve({}); + }; + + const _backendSrv = new BackendSrv(_httpBackend, {}, {}, {}, {}); + + describe('when handling errors', () => { + it('should return the http status code', async () => { + try { + await _backendSrv.datasourceRequest({ + url: 'gateway-error', + }); + } catch (err) { + expect(err.status).toBe(502); + } + }); + }); +}); diff --git a/public/app/core/specs/backend_srv_specs.ts b/public/app/core/specs/backend_srv_specs.ts deleted file mode 100644 index 74b058b98c8..00000000000 --- a/public/app/core/specs/backend_srv_specs.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common'; -import 'app/core/services/backend_srv'; - -describe('backend_srv', function() { - var _backendSrv; - var _httpBackend; - - beforeEach(angularMocks.module('grafana.core')); - beforeEach(angularMocks.module('grafana.services')); - beforeEach( - angularMocks.inject(function($httpBackend, $http, backendSrv) { - _httpBackend = $httpBackend; - _backendSrv = backendSrv; - }) - ); - - describe('when handling errors', function() { - it('should return the http status code', function(done) { - _httpBackend.whenGET('gateway-error').respond(502); - _backendSrv - .datasourceRequest({ - url: 'gateway-error', - }) - .catch(function(err) { - expect(err.status).to.be(502); - done(); - }); - _httpBackend.flush(); - }); - }); -}); diff --git a/public/app/core/specs/datemath.jest.ts b/public/app/core/specs/datemath.test.ts similarity index 80% rename from public/app/core/specs/datemath.jest.ts rename to public/app/core/specs/datemath.test.ts index 820c53486db..cca6eb31593 100644 --- a/public/app/core/specs/datemath.jest.ts +++ b/public/app/core/specs/datemath.test.ts @@ -5,11 +5,11 @@ import moment from 'moment'; import _ from 'lodash'; describe('DateMath', () => { - var spans = ['s', 'm', 'h', 'd', 'w', 'M', 'y']; - var anchor = '2014-01-01T06:06:06.666Z'; - var unix = moment(anchor).valueOf(); - var format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; - var clock; + const spans = ['s', 'm', 'h', 'd', 'w', 'M', 'y']; + const anchor = '2014-01-01T06:06:06.666Z'; + const unix = moment(anchor).valueOf(); + const format = 'YYYY-MM-DDTHH:mm:ss.SSSZ'; + let clock; describe('errors', () => { it('should return undefined if passed something falsy', () => { @@ -36,21 +36,21 @@ describe('DateMath', () => { }); it('now/d should set to start of current day', () => { - var expected = new Date(); + const expected = new Date(); expected.setHours(0); expected.setMinutes(0); expected.setSeconds(0); expected.setMilliseconds(0); - var startOfDay = dateMath.parse('now/d', false).valueOf(); + const startOfDay = dateMath.parse('now/d', false).valueOf(); expect(startOfDay).toBe(expected.getTime()); }); it('now/d on a utc dashboard should be start of the current day in UTC time', () => { - var today = new Date(); - var expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0)); + const today = new Date(); + const expected = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate(), 0, 0, 0, 0)); - var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf(); + const startOfDay = dateMath.parse('now/d', false, 'utc').valueOf(); expect(startOfDay).toBe(expected.getTime()); }); @@ -65,8 +65,8 @@ describe('DateMath', () => { }); _.each(spans, span => { - var nowEx = 'now-5' + span; - var thenEx = anchor + '||-5' + span; + const nowEx = 'now-5' + span; + const thenEx = anchor + '||-5' + span; it('should return 5' + span + ' ago', () => { expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format)); @@ -116,17 +116,17 @@ describe('DateMath', () => { describe('relative time to date parsing', function() { it('should handle negative time', function() { - var date = dateMath.parseDateMath('-2d', moment([2014, 1, 5])); + const date = dateMath.parseDateMath('-2d', moment([2014, 1, 5])); expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf()); }); it('should handle multiple math expressions', function() { - var date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5])); + const date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5])); expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf()); }); it('should return false when invalid expression', function() { - var date = dateMath.parseDateMath('2', moment([2014, 1, 5])); + const date = dateMath.parseDateMath('2', moment([2014, 1, 5])); expect(date).toEqual(undefined); }); }); diff --git a/public/app/core/specs/emitter.jest.ts b/public/app/core/specs/emitter.test.ts similarity index 81% rename from public/app/core/specs/emitter.jest.ts rename to public/app/core/specs/emitter.test.ts index a819cf0ede2..549dcb2d84a 100644 --- a/public/app/core/specs/emitter.jest.ts +++ b/public/app/core/specs/emitter.test.ts @@ -3,9 +3,9 @@ import { Emitter } from '../utils/emitter'; describe('Emitter', () => { describe('given 2 subscribers', () => { it('should notfiy subscribers', () => { - var events = new Emitter(); - var sub1Called = false; - var sub2Called = false; + const events = new Emitter(); + let sub1Called = false; + let sub2Called = false; events.on('test', () => { sub1Called = true; @@ -21,8 +21,8 @@ describe('Emitter', () => { }); it('when subscribing twice', () => { - var events = new Emitter(); - var sub1Called = 0; + const events = new Emitter(); + let sub1Called = 0; function handler() { sub1Called += 1; @@ -37,9 +37,9 @@ describe('Emitter', () => { }); it('should handle errors', () => { - var events = new Emitter(); - var sub1Called = 0; - var sub2Called = 0; + const events = new Emitter(); + let sub1Called = 0; + let sub2Called = 0; events.on('test', () => { sub1Called++; diff --git a/public/app/core/specs/file_export.jest.ts b/public/app/core/specs/file_export.test.ts similarity index 95% rename from public/app/core/specs/file_export.jest.ts rename to public/app/core/specs/file_export.test.ts index 915ce08fcd2..ced94fcdbc0 100644 --- a/public/app/core/specs/file_export.jest.ts +++ b/public/app/core/specs/file_export.test.ts @@ -2,7 +2,7 @@ import * as fileExport from '../utils/file_export'; import { beforeEach, expect } from 'test/lib/common'; describe('file_export', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.seriesList = [ @@ -28,7 +28,7 @@ describe('file_export', () => { describe('when exporting series as rows', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsv(ctx.seriesList, ctx.timeFormat); const expectedText = '"Series";"Time";"Value"\r\n' + '"series_1";"1500026100";1\r\n' + @@ -48,7 +48,7 @@ describe('file_export', () => { describe('when exporting series as columns', () => { it('should export points in proper order', () => { - let text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); + const text = fileExport.convertSeriesListToCsvColumns(ctx.seriesList, ctx.timeFormat); const expectedText = '"Time";"series_1";"series_2"\r\n' + '"1500026100";1;11\r\n' + diff --git a/public/app/core/specs/flatten.jest.ts b/public/app/core/specs/flatten.test.ts similarity index 94% rename from public/app/core/specs/flatten.jest.ts rename to public/app/core/specs/flatten.test.ts index 7c7f4816d94..f1c5237802d 100644 --- a/public/app/core/specs/flatten.jest.ts +++ b/public/app/core/specs/flatten.test.ts @@ -2,7 +2,7 @@ import flatten from 'app/core/utils/flatten'; describe('flatten', () => { it('should return flatten object', () => { - var flattened = flatten( + const flattened = flatten( { level1: 'level1-value', deeper: { diff --git a/public/app/core/specs/kbn.jest.ts b/public/app/core/specs/kbn.test.ts similarity index 66% rename from public/app/core/specs/kbn.jest.ts rename to public/app/core/specs/kbn.test.ts index 68945068043..b4072adf7cf 100644 --- a/public/app/core/specs/kbn.jest.ts +++ b/public/app/core/specs/kbn.test.ts @@ -3,7 +3,7 @@ import * as dateMath from '../utils/datemath'; import moment from 'moment'; describe('unit format menu', function() { - var menu = kbn.getUnitFormats(); + const menu = kbn.getUnitFormats(); menu.map(function(submenu) { describe('submenu ' + submenu.text, function() { it('should have a title', function() { @@ -34,8 +34,8 @@ describe('unit format menu', function() { function describeValueFormat(desc, value, tickSize, tickDecimals, result) { describe('value format: ' + desc, function() { it('should translate ' + value + ' as ' + result, function() { - var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); - var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); + const scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10); + const str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals); expect(str).toBe(result); }); }); @@ -106,177 +106,177 @@ describe('date time formats', function() { const browserTime = moment(epoch); it('should format as iso date', function() { - var expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); - var actual = kbn.valueFormats.dateTimeAsIso(epoch); + const expected = browserTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = kbn.valueFormats.dateTimeAsIso(epoch); expect(actual).toBe(expected); }); it('should format as iso date (in UTC)', function() { - var expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); - var actual = kbn.valueFormats.dateTimeAsIso(epoch, true); + const expected = utcTime.format('YYYY-MM-DD HH:mm:ss'); + const actual = kbn.valueFormats.dateTimeAsIso(epoch, true); expect(actual).toBe(expected); }); it('should format as iso date and skip date when today', function() { - var now = moment(); - var expected = now.format('HH:mm:ss'); - var actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); + const now = moment(); + const expected = now.format('HH:mm:ss'); + const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), false); expect(actual).toBe(expected); }); it('should format as iso date (in UTC) and skip date when today', function() { - var now = moment.utc(); - var expected = now.format('HH:mm:ss'); - var actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); + const now = moment.utc(); + const expected = now.format('HH:mm:ss'); + const actual = kbn.valueFormats.dateTimeAsIso(now.valueOf(), true); expect(actual).toBe(expected); }); it('should format as US date', function() { - var expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); - var actual = kbn.valueFormats.dateTimeAsUS(epoch, false); + const expected = browserTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = kbn.valueFormats.dateTimeAsUS(epoch, false); expect(actual).toBe(expected); }); it('should format as US date (in UTC)', function() { - var expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); - var actual = kbn.valueFormats.dateTimeAsUS(epoch, true); + const expected = utcTime.format('MM/DD/YYYY h:mm:ss a'); + const actual = kbn.valueFormats.dateTimeAsUS(epoch, true); expect(actual).toBe(expected); }); it('should format as US date and skip date when today', function() { - var now = moment(); - var expected = now.format('h:mm:ss a'); - var actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); + const now = moment(); + const expected = now.format('h:mm:ss a'); + const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), false); expect(actual).toBe(expected); }); it('should format as US date (in UTC) and skip date when today', function() { - var now = moment.utc(); - var expected = now.format('h:mm:ss a'); - var actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); + const now = moment.utc(); + const expected = now.format('h:mm:ss a'); + const actual = kbn.valueFormats.dateTimeAsUS(now.valueOf(), true); expect(actual).toBe(expected); }); it('should format as from now with days', function() { - var daysAgo = moment().add(-7, 'd'); - var expected = '7 days ago'; - var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); + const daysAgo = moment().add(-7, 'd'); + const expected = '7 days ago'; + const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); expect(actual).toBe(expected); }); it('should format as from now with days (in UTC)', function() { - var daysAgo = moment.utc().add(-7, 'd'); - var expected = '7 days ago'; - var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); + const daysAgo = moment.utc().add(-7, 'd'); + const expected = '7 days ago'; + const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); expect(actual).toBe(expected); }); it('should format as from now with minutes', function() { - var daysAgo = moment().add(-2, 'm'); - var expected = '2 minutes ago'; - var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); + const daysAgo = moment().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), false); expect(actual).toBe(expected); }); it('should format as from now with minutes (in UTC)', function() { - var daysAgo = moment.utc().add(-2, 'm'); - var expected = '2 minutes ago'; - var actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); + const daysAgo = moment.utc().add(-2, 'm'); + const expected = '2 minutes ago'; + const actual = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), true); expect(actual).toBe(expected); }); }); describe('kbn.toFixed and negative decimals', function() { it('should treat as zero decimals', function() { - var str = kbn.toFixed(186.123, -2); + const str = kbn.toFixed(186.123, -2); expect(str).toBe('186'); }); }); describe('kbn ms format when scaled decimals is null do not use it', function() { it('should use specified decimals', function() { - var str = kbn.valueFormats['ms'](10000086.123, 1, null); + const str = kbn.valueFormats['ms'](10000086.123, 1, null); expect(str).toBe('2.8 hour'); }); }); describe('kbn kbytes format when scaled decimals is null do not use it', function() { it('should use specified decimals', function() { - var str = kbn.valueFormats['kbytes'](10000000, 3, null); + const str = kbn.valueFormats['kbytes'](10000000, 3, null); expect(str).toBe('9.537 GiB'); }); }); describe('kbn deckbytes format when scaled decimals is null do not use it', function() { it('should use specified decimals', function() { - var str = kbn.valueFormats['deckbytes'](10000000, 3, null); + const str = kbn.valueFormats['deckbytes'](10000000, 3, null); expect(str).toBe('10.000 GB'); }); }); describe('kbn roundValue', function() { it('should should handle null value', function() { - var str = kbn.roundValue(null, 2); + const str = kbn.roundValue(null, 2); expect(str).toBe(null); }); it('should round value', function() { - var str = kbn.roundValue(200.877, 2); + const str = kbn.roundValue(200.877, 2); expect(str).toBe(200.88); }); }); describe('calculateInterval', function() { it('1h 100 resultion', function() { - var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 100, null); + const range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 100, null); expect(res.interval).toBe('30s'); }); it('10m 1600 resolution', function() { - var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 1600, null); + const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 1600, null); expect(res.interval).toBe('500ms'); expect(res.intervalMs).toBe(500); }); it('fixed user min interval', function() { - var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 1600, '10s'); + const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 1600, '10s'); expect(res.interval).toBe('10s'); expect(res.intervalMs).toBe(10000); }); it('short time range and user low limit', function() { - var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 1600, '>10s'); + const range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 1600, '>10s'); expect(res.interval).toBe('10s'); }); it('large time range and user low limit', function() { - var range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 1000, '>10s'); + const range = { from: dateMath.parse('now-14d'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 1000, '>10s'); expect(res.interval).toBe('20m'); }); it('10s 900 resolution and user low limit in ms', function() { - var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 900, '>15ms'); + const range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 900, '>15ms'); expect(res.interval).toBe('15ms'); }); it('1d 1 resolution', function() { - var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; - var res = kbn.calculateInterval(range, 1, null); + const range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') }; + const res = kbn.calculateInterval(range, 1, null); expect(res.interval).toBe('1d'); expect(res.intervalMs).toBe(86400000); }); it('86399s 1 resolution', function() { - var range = { + const range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now'), }; - var res = kbn.calculateInterval(range, 1, null); + const res = kbn.calculateInterval(range, 1, null); expect(res.interval).toBe('12h'); expect(res.intervalMs).toBe(43200000); }); @@ -284,139 +284,139 @@ describe('calculateInterval', function() { describe('hex', function() { it('positive integer', function() { - var str = kbn.valueFormats.hex(100, 0); + const str = kbn.valueFormats.hex(100, 0); expect(str).toBe('64'); }); it('negative integer', function() { - var str = kbn.valueFormats.hex(-100, 0); + const str = kbn.valueFormats.hex(-100, 0); expect(str).toBe('-64'); }); it('null', function() { - var str = kbn.valueFormats.hex(null, 0); + const str = kbn.valueFormats.hex(null, 0); expect(str).toBe(''); }); it('positive float', function() { - var str = kbn.valueFormats.hex(50.52, 1); + const str = kbn.valueFormats.hex(50.52, 1); expect(str).toBe('32.8'); }); it('negative float', function() { - var str = kbn.valueFormats.hex(-50.333, 2); + const str = kbn.valueFormats.hex(-50.333, 2); expect(str).toBe('-32.547AE147AE14'); }); }); describe('hex 0x', function() { it('positive integeter', function() { - var str = kbn.valueFormats.hex0x(7999, 0); + const str = kbn.valueFormats.hex0x(7999, 0); expect(str).toBe('0x1F3F'); }); it('negative integer', function() { - var str = kbn.valueFormats.hex0x(-584, 0); + const str = kbn.valueFormats.hex0x(-584, 0); expect(str).toBe('-0x248'); }); it('null', function() { - var str = kbn.valueFormats.hex0x(null, 0); + const str = kbn.valueFormats.hex0x(null, 0); expect(str).toBe(''); }); it('positive float', function() { - var str = kbn.valueFormats.hex0x(74.443, 3); + const str = kbn.valueFormats.hex0x(74.443, 3); expect(str).toBe('0x4A.716872B020C4'); }); it('negative float', function() { - var str = kbn.valueFormats.hex0x(-65.458, 1); + const str = kbn.valueFormats.hex0x(-65.458, 1); expect(str).toBe('-0x41.8'); }); }); describe('duration', function() { it('null', function() { - var str = kbn.toDuration(null, 0, 'millisecond'); + const str = kbn.toDuration(null, 0, 'millisecond'); expect(str).toBe(''); }); it('0 milliseconds', function() { - var str = kbn.toDuration(0, 0, 'millisecond'); + const str = kbn.toDuration(0, 0, 'millisecond'); expect(str).toBe('0 milliseconds'); }); it('1 millisecond', function() { - var str = kbn.toDuration(1, 0, 'millisecond'); + const str = kbn.toDuration(1, 0, 'millisecond'); expect(str).toBe('1 millisecond'); }); it('-1 millisecond', function() { - var str = kbn.toDuration(-1, 0, 'millisecond'); + const str = kbn.toDuration(-1, 0, 'millisecond'); expect(str).toBe('1 millisecond ago'); }); it('seconds', function() { - var str = kbn.toDuration(1, 0, 'second'); + const str = kbn.toDuration(1, 0, 'second'); expect(str).toBe('1 second'); }); it('minutes', function() { - var str = kbn.toDuration(1, 0, 'minute'); + const str = kbn.toDuration(1, 0, 'minute'); expect(str).toBe('1 minute'); }); it('hours', function() { - var str = kbn.toDuration(1, 0, 'hour'); + const str = kbn.toDuration(1, 0, 'hour'); expect(str).toBe('1 hour'); }); it('days', function() { - var str = kbn.toDuration(1, 0, 'day'); + const str = kbn.toDuration(1, 0, 'day'); expect(str).toBe('1 day'); }); it('weeks', function() { - var str = kbn.toDuration(1, 0, 'week'); + const str = kbn.toDuration(1, 0, 'week'); expect(str).toBe('1 week'); }); it('months', function() { - var str = kbn.toDuration(1, 0, 'month'); + const str = kbn.toDuration(1, 0, 'month'); expect(str).toBe('1 month'); }); it('years', function() { - var str = kbn.toDuration(1, 0, 'year'); + const str = kbn.toDuration(1, 0, 'year'); expect(str).toBe('1 year'); }); it('decimal days', function() { - var str = kbn.toDuration(1.5, 2, 'day'); + const str = kbn.toDuration(1.5, 2, 'day'); expect(str).toBe('1 day, 12 hours, 0 minutes'); }); it('decimal months', function() { - var str = kbn.toDuration(1.5, 3, 'month'); + const str = kbn.toDuration(1.5, 3, 'month'); expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours'); }); it('no decimals', function() { - var str = kbn.toDuration(38898367008, 0, 'millisecond'); + const str = kbn.toDuration(38898367008, 0, 'millisecond'); expect(str).toBe('1 year'); }); it('1 decimal', function() { - var str = kbn.toDuration(38898367008, 1, 'millisecond'); + const str = kbn.toDuration(38898367008, 1, 'millisecond'); expect(str).toBe('1 year, 2 months'); }); it('too many decimals', function() { - var str = kbn.toDuration(38898367008, 20, 'millisecond'); + const str = kbn.toDuration(38898367008, 20, 'millisecond'); expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds'); }); it('floating point error', function() { - var str = kbn.toDuration(36993906007, 8, 'millisecond'); + const str = kbn.toDuration(36993906007, 8, 'millisecond'); expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds'); }); }); describe('volume', function() { it('1000m3', function() { - var str = kbn.valueFormats['m3'](1000, 1, null); - expect(str).toBe('1000.0 m3'); + const str = kbn.valueFormats['m3'](1000, 1, null); + expect(str).toBe('1000.0 m³'); }); }); describe('hh:mm:ss', function() { it('00:04:06', function() { - var str = kbn.valueFormats['dthms'](246, 1); + const str = kbn.valueFormats['dthms'](246, 1); expect(str).toBe('00:04:06'); }); it('24:00:00', function() { - var str = kbn.valueFormats['dthms'](86400, 1); + const str = kbn.valueFormats['dthms'](86400, 1); expect(str).toBe('24:00:00'); }); it('6824413:53:20', function() { - var str = kbn.valueFormats['dthms'](24567890000, 1); + const str = kbn.valueFormats['dthms'](24567890000, 1); expect(str).toBe('6824413:53:20'); }); }); diff --git a/public/app/core/specs/location_util.jest.ts b/public/app/core/specs/location_util.test.ts similarity index 100% rename from public/app/core/specs/location_util.jest.ts rename to public/app/core/specs/location_util.test.ts diff --git a/public/app/core/specs/manage_dashboards.jest.ts b/public/app/core/specs/manage_dashboards.test.ts similarity index 100% rename from public/app/core/specs/manage_dashboards.jest.ts rename to public/app/core/specs/manage_dashboards.test.ts diff --git a/public/app/core/specs/org_switcher.jest.ts b/public/app/core/specs/org_switcher.test.ts similarity index 100% rename from public/app/core/specs/org_switcher.jest.ts rename to public/app/core/specs/org_switcher.test.ts diff --git a/public/app/core/specs/rangeutil.jest.ts b/public/app/core/specs/rangeutil.test.ts similarity index 72% rename from public/app/core/specs/rangeutil.jest.ts rename to public/app/core/specs/rangeutil.test.ts index 6bfc0503900..4c0d3dc90c8 100644 --- a/public/app/core/specs/rangeutil.jest.ts +++ b/public/app/core/specs/rangeutil.test.ts @@ -5,7 +5,7 @@ import moment from 'moment'; describe('rangeUtil', () => { describe('Can get range grouped list of ranges', () => { it('when custom settings should return default range list', () => { - var groups = rangeUtil.getRelativeTimesList({ time_options: [] }, 'Last 5 minutes'); + const groups = rangeUtil.getRelativeTimesList({ time_options: [] }, 'Last 5 minutes'); expect(_.keys(groups).length).toBe(4); expect(groups[3][0].active).toBe(true); }); @@ -13,62 +13,62 @@ describe('rangeUtil', () => { describe('Can get range text described', () => { it('should handle simple old expression with only amount and unit', () => { - var info = rangeUtil.describeTextRange('5m'); + const info = rangeUtil.describeTextRange('5m'); expect(info.display).toBe('Last 5 minutes'); }); it('should have singular when amount is 1', () => { - var info = rangeUtil.describeTextRange('1h'); + const info = rangeUtil.describeTextRange('1h'); expect(info.display).toBe('Last 1 hour'); }); it('should handle non default amount', () => { - var info = rangeUtil.describeTextRange('13h'); + const info = rangeUtil.describeTextRange('13h'); expect(info.display).toBe('Last 13 hours'); expect(info.from).toBe('now-13h'); }); it('should handle non default future amount', () => { - var info = rangeUtil.describeTextRange('+3h'); + const info = rangeUtil.describeTextRange('+3h'); expect(info.display).toBe('Next 3 hours'); expect(info.from).toBe('now'); expect(info.to).toBe('now+3h'); }); it('should handle now/d', () => { - var info = rangeUtil.describeTextRange('now/d'); + const info = rangeUtil.describeTextRange('now/d'); expect(info.display).toBe('Today so far'); }); it('should handle now/w', () => { - var info = rangeUtil.describeTextRange('now/w'); + const info = rangeUtil.describeTextRange('now/w'); expect(info.display).toBe('This week so far'); }); it('should handle now/M', () => { - var info = rangeUtil.describeTextRange('now/M'); + const info = rangeUtil.describeTextRange('now/M'); expect(info.display).toBe('This month so far'); }); it('should handle now/y', () => { - var info = rangeUtil.describeTextRange('now/y'); + const info = rangeUtil.describeTextRange('now/y'); expect(info.display).toBe('This year so far'); }); }); describe('Can get date range described', () => { it('Date range with simple ranges', () => { - var text = rangeUtil.describeTimeRange({ from: 'now-1h', to: 'now' }); + const text = rangeUtil.describeTimeRange({ from: 'now-1h', to: 'now' }); expect(text).toBe('Last 1 hour'); }); it('Date range with rounding ranges', () => { - var text = rangeUtil.describeTimeRange({ from: 'now/d+6h', to: 'now' }); + const text = rangeUtil.describeTimeRange({ from: 'now/d+6h', to: 'now' }); expect(text).toBe('now/d+6h to now'); }); it('Date range with absolute to now', () => { - var text = rangeUtil.describeTimeRange({ + const text = rangeUtil.describeTimeRange({ from: moment([2014, 10, 10, 2, 3, 4]), to: 'now', }); @@ -76,7 +76,7 @@ describe('rangeUtil', () => { }); it('Date range with absolute to relative', () => { - var text = rangeUtil.describeTimeRange({ + const text = rangeUtil.describeTimeRange({ from: moment([2014, 10, 10, 2, 3, 4]), to: 'now-1d', }); @@ -84,7 +84,7 @@ describe('rangeUtil', () => { }); it('Date range with relative to absolute', () => { - var text = rangeUtil.describeTimeRange({ + const text = rangeUtil.describeTimeRange({ from: 'now-7d', to: moment([2014, 10, 10, 2, 3, 4]), }); @@ -92,17 +92,17 @@ describe('rangeUtil', () => { }); it('Date range with non matching default ranges', () => { - var text = rangeUtil.describeTimeRange({ from: 'now-13h', to: 'now' }); + const text = rangeUtil.describeTimeRange({ from: 'now-13h', to: 'now' }); expect(text).toBe('Last 13 hours'); }); it('Date range with from and to both are in now-* format', () => { - var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now-3h' }); + const text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now-3h' }); expect(text).toBe('now-6h to now-3h'); }); it('Date range with from and to both are either in now-* or now/* format', () => { - var text = rangeUtil.describeTimeRange({ + const text = rangeUtil.describeTimeRange({ from: 'now/d+6h', to: 'now-3h', }); @@ -110,7 +110,7 @@ describe('rangeUtil', () => { }); it('Date range with from and to both are either in now-* or now+* format', () => { - var text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now+1h' }); + const text = rangeUtil.describeTimeRange({ from: 'now-6h', to: 'now+1h' }); expect(text).toBe('now-6h to now+1h'); }); }); diff --git a/public/app/core/specs/search.jest.ts b/public/app/core/specs/search.test.ts similarity index 99% rename from public/app/core/specs/search.jest.ts rename to public/app/core/specs/search.test.ts index 8aea35af213..3cc789b3cc5 100644 --- a/public/app/core/specs/search.jest.ts +++ b/public/app/core/specs/search.test.ts @@ -12,7 +12,7 @@ describe('SearchCtrl', () => { search: (options: any) => {}, getDashboardTags: () => {}, }; - let ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); + const ctrl = new SearchCtrl({ $on: () => {} }, {}, {}, searchSrvStub); describe('Given an empty result', () => { beforeEach(() => { diff --git a/public/app/core/specs/search_results.jest.ts b/public/app/core/specs/search_results.test.ts similarity index 96% rename from public/app/core/specs/search_results.jest.ts rename to public/app/core/specs/search_results.test.ts index 830496be3a8..96dbc8bb963 100644 --- a/public/app/core/specs/search_results.jest.ts +++ b/public/app/core/specs/search_results.test.ts @@ -12,7 +12,7 @@ describe('SearchResultsCtrl', () => { let ctrl; describe('when checking an item that is not checked', () => { - let item = { checked: false }; + const item = { checked: false }; let selectionChanged = false; beforeEach(() => { @@ -31,7 +31,7 @@ describe('SearchResultsCtrl', () => { }); describe('when checking an item that is checked', () => { - let item = { checked: true }; + const item = { checked: true }; let selectionChanged = false; beforeEach(() => { @@ -72,7 +72,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: false, toggle: () => Promise.resolve(folder), }; @@ -94,7 +94,7 @@ describe('SearchResultsCtrl', () => { folderExpanded = true; }; - let folder = { + const folder = { expanded: true, toggle: () => Promise.resolve(folder), }; diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.test.ts similarity index 100% rename from public/app/core/specs/search_srv.jest.ts rename to public/app/core/specs/search_srv.test.ts diff --git a/public/app/core/specs/store.jest.ts b/public/app/core/specs/store.test.ts similarity index 68% rename from public/app/core/specs/store.jest.ts rename to public/app/core/specs/store.test.ts index 0162960621d..ac02501f99e 100644 --- a/public/app/core/specs/store.jest.ts +++ b/public/app/core/specs/store.test.ts @@ -32,6 +32,18 @@ describe('store', () => { expect(store.getBool('key5', false)).toBe(true); }); + it('gets an object', () => { + expect(store.getObject('object1')).toBeUndefined(); + expect(store.getObject('object1', [])).toEqual([]); + store.setObject('object1', [1]); + expect(store.getObject('object1')).toEqual([1]); + }); + + it('sets an object', () => { + expect(store.setObject('object2', { a: 1 })).toBe(true); + expect(store.getObject('object2')).toEqual({ a: 1 }); + }); + it('key should be deleted', () => { store.set('key6', '123'); store.delete('key6'); diff --git a/public/app/core/specs/table_model.jest.ts b/public/app/core/specs/table_model.test.ts similarity index 97% rename from public/app/core/specs/table_model.jest.ts rename to public/app/core/specs/table_model.test.ts index 3d4c526cfea..b88bad12227 100644 --- a/public/app/core/specs/table_model.jest.ts +++ b/public/app/core/specs/table_model.test.ts @@ -1,8 +1,8 @@ import TableModel from 'app/core/table_model'; describe('when sorting table desc', () => { - var table; - var panel = { + let table; + const panel = { sort: { col: 0, desc: true }, }; @@ -27,7 +27,7 @@ describe('when sorting table desc', () => { describe('when sorting table asc', () => { var table; - var panel = { + const panel = { sort: { col: 1, desc: false }, }; diff --git a/public/app/core/specs/ticks.jest.ts b/public/app/core/specs/ticks.test.ts similarity index 96% rename from public/app/core/specs/ticks.jest.ts rename to public/app/core/specs/ticks.test.ts index 8b7e0cd73b5..73d0e96cbd2 100644 --- a/public/app/core/specs/ticks.jest.ts +++ b/public/app/core/specs/ticks.test.ts @@ -2,7 +2,7 @@ import * as ticks from '../utils/ticks'; describe('ticks', () => { describe('getFlotTickDecimals()', () => { - let ctx: any = {}; + const ctx: any = {}; beforeEach(() => { ctx.axis = {}; diff --git a/public/app/core/specs/time_series.jest.ts b/public/app/core/specs/time_series.test.ts similarity index 98% rename from public/app/core/specs/time_series.jest.ts rename to public/app/core/specs/time_series.test.ts index bf50d807e03..605c8fd0a2e 100644 --- a/public/app/core/specs/time_series.jest.ts +++ b/public/app/core/specs/time_series.test.ts @@ -2,9 +2,9 @@ import TimeSeries from 'app/core/time_series2'; import { updateLegendValues } from 'app/core/time_series2'; describe('TimeSeries', function() { - var points, series; - var yAxisFormats = ['short', 'ms']; - var testData; + let points, series; + const yAxisFormats = ['short', 'ms']; + let testData; beforeEach(function() { testData = { @@ -329,7 +329,7 @@ describe('TimeSeries', function() { describe('legend decimals', function() { let series, panel; - let height = 200; + const height = 200; beforeEach(function() { testData = { alias: 'test', @@ -348,7 +348,7 @@ describe('TimeSeries', function() { }); it('should set decimals based on Y axis (expect calculated decimals = 1)', function() { - let data = [series]; + const data = [series]; // Expect ticks with this data will have decimals = 1 updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(2); @@ -358,21 +358,21 @@ describe('TimeSeries', function() { testData.datapoints = [[10, 2], [0, 3], [100, 4], [80, 5]]; series = new TimeSeries(testData); series.getFlotPairs(); - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(0); }); it('should set decimals to Y axis decimals + 1', function() { panel.yaxes[0].decimals = 2; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); it('should set decimals to legend decimals value if it was set explicitly', function() { panel.decimals = 3; - let data = [series]; + const data = [series]; updateLegendValues(data, panel, height); expect(data[0].decimals).toBe(3); }); diff --git a/public/app/core/specs/value_select_dropdown.jest.ts b/public/app/core/specs/value_select_dropdown.test.ts similarity index 99% rename from public/app/core/specs/value_select_dropdown.jest.ts rename to public/app/core/specs/value_select_dropdown.test.ts index 3cc310435b7..024774250b8 100644 --- a/public/app/core/specs/value_select_dropdown.jest.ts +++ b/public/app/core/specs/value_select_dropdown.test.ts @@ -3,7 +3,7 @@ import { ValueSelectDropdownCtrl } from '../directives/value_select_dropdown'; import q from 'q'; describe('SelectDropdownCtrl', () => { - let tagValuesMap: any = {}; + const tagValuesMap: any = {}; ValueSelectDropdownCtrl.prototype.onUpdated = jest.fn(); let ctrl; diff --git a/public/app/core/store.ts b/public/app/core/store.ts index b0714f49256..7cc969cf97f 100644 --- a/public/app/core/store.ts +++ b/public/app/core/store.ts @@ -14,6 +14,38 @@ export class Store { return window.localStorage[key] === 'true'; } + getObject(key: string, def?: any) { + let ret = def; + if (this.exists(key)) { + const json = window.localStorage[key]; + try { + ret = JSON.parse(json); + } catch (error) { + console.error(`Error parsing store object: ${key}. Returning default: ${def}. [${error}]`); + } + } + return ret; + } + + // Returns true when successfully stored + setObject(key: string, value: any): boolean { + let json; + try { + json = JSON.stringify(value); + } catch (error) { + console.error(`Could not stringify object: ${key}. [${error}]`); + return false; + } + try { + this.set(key, json); + } catch (error) { + // Likely hitting storage quota + console.error(`Could not save item in localStorage: ${key}. [${error}]`); + return false; + } + return true; + } + exists(key) { return window.localStorage[key] !== void 0; } diff --git a/public/app/core/table_model.ts b/public/app/core/table_model.ts index 04857eb806d..0c85a0293dd 100644 --- a/public/app/core/table_model.ts +++ b/public/app/core/table_model.ts @@ -1,5 +1,15 @@ +interface Column { + text: string; + title?: string; + type?: string; + sort?: boolean; + desc?: boolean; + filterable?: boolean; + unit?: string; +} + export default class TableModel { - columns: any[]; + columns: Column[]; rows: any[]; type: string; columnMap: any; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 59729ebc312..c29242c9aca 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -27,11 +27,11 @@ function translateFillOption(fill) { */ export function updateLegendValues(data: TimeSeries[], panel, height) { for (let i = 0; i < data.length; i++) { - let series = data[i]; + const series = data[i]; const yaxes = panel.yaxes; const seriesYAxis = series.yaxis || 1; const axis = yaxes[seriesYAxis - 1]; - let formater = kbn.valueFormats[axis.format]; + const formater = kbn.valueFormats[axis.format]; // decimal override if (_.isNumber(panel.decimals)) { @@ -54,7 +54,7 @@ export function getDataMinMax(data: TimeSeries[]) { let datamin = null; let datamax = null; - for (let series of data) { + for (const series of data) { if (datamax === null || datamax < series.stats.max) { datamax = series.stats.max; } @@ -76,6 +76,7 @@ export default class TimeSeries { valueFormater: any; stats: any; legend: boolean; + hideTooltip: boolean; allIsNull: boolean; allIsZero: boolean; decimals: number; @@ -181,6 +182,9 @@ export default class TimeSeries { if (override.legend !== void 0) { this.legend = override.legend; } + if (override.hideTooltip !== void 0) { + this.hideTooltip = override.hideTooltip; + } if (override.yaxis !== void 0) { this.yaxis = override.yaxis; @@ -221,7 +225,7 @@ export default class TimeSeries { // Due to missing values we could have different timeStep all along the series // so we have to find the minimum one (could occur with aggregators such as ZimSum) if (previousTime !== undefined) { - let timeStep = currentTime - previousTime; + const timeStep = currentTime - previousTime; if (timeStep < this.stats.timeStep) { this.stats.timeStep = timeStep; } diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index 8a70e093ea2..e8a7366beb5 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -9,7 +9,7 @@ export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; export const REGION_FILL_ALPHA = 0.09; -let colors = [ +const colors = [ '#7EB26D', '#EAB839', '#6ED0E0', @@ -69,7 +69,7 @@ let colors = [ ]; export function sortColorsByHue(hexColors) { - let hslColors = _.map(hexColors, hexToHsl); + const hslColors = _.map(hexColors, hexToHsl); let sortedHSLColors = _.sortBy(hslColors, ['h']); sortedHSLColors = _.chunk(sortedHSLColors, PALETTE_ROWS); diff --git a/public/app/core/utils/css_loader.ts b/public/app/core/utils/css_loader.ts index ba8623df842..b8aaef47085 100644 --- a/public/app/core/utils/css_loader.ts +++ b/public/app/core/utils/css_loader.ts @@ -1,18 +1,18 @@ -var waitSeconds = 100; -var head = document.getElementsByTagName('head')[0]; +const waitSeconds = 100; +const head = document.getElementsByTagName('head')[0]; // get all link tags in the page -var links = document.getElementsByTagName('link'); -var linkHrefs = []; -for (var i = 0; i < links.length; i++) { +const links = document.getElementsByTagName('link'); +const linkHrefs = []; +for (let i = 0; i < links.length; i++) { linkHrefs.push(links[i].href); } -var isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/); -var webkitLoadCheck = function(link, callback) { +const isWebkit = !!window.navigator.userAgent.match(/AppleWebKit\/([^ ;]*)/); +const webkitLoadCheck = function(link, callback) { setTimeout(function() { for (var i = 0; i < document.styleSheets.length; i++) { - var sheet = document.styleSheets[i]; + const sheet = document.styleSheets[i]; if (sheet.href === link.href) { return callback(); } @@ -21,16 +21,16 @@ var webkitLoadCheck = function(link, callback) { }, 10); }; -var noop = function() {}; +const noop = function() {}; -var loadCSS = function(url) { +const loadCSS = function(url) { return new Promise(function(resolve, reject) { - var link = document.createElement('link'); - var timeout = setTimeout(function() { + const link = document.createElement('link'); + const timeout = setTimeout(function() { reject('Unable to load CSS'); }, waitSeconds * 1000); - var _callback = function(error) { + const _callback = function(error) { clearTimeout(timeout); link.onload = link.onerror = noop; setTimeout(function() { diff --git a/public/app/core/utils/dag.test.ts b/public/app/core/utils/dag.test.ts new file mode 100644 index 00000000000..064da13806b --- /dev/null +++ b/public/app/core/utils/dag.test.ts @@ -0,0 +1,108 @@ +import { Graph } from './dag'; + +describe('Directed acyclic graph', () => { + describe('Given a graph with nodes with different links in between them', () => { + const dag = new Graph(); + const nodeA = dag.createNode('A'); + const nodeB = dag.createNode('B'); + const nodeC = dag.createNode('C'); + const nodeD = dag.createNode('D'); + const nodeE = dag.createNode('E'); + const nodeF = dag.createNode('F'); + const nodeG = dag.createNode('G'); + const nodeH = dag.createNode('H'); + const nodeI = dag.createNode('I'); + dag.link([nodeB, nodeC, nodeD, nodeE, nodeF, nodeG, nodeH], nodeA); + dag.link([nodeC, nodeD, nodeE, nodeF, nodeI], nodeB); + dag.link([nodeD, nodeE, nodeF, nodeG], nodeC); + dag.link([nodeE, nodeF], nodeD); + dag.link([nodeF, nodeG], nodeE); + //printGraph(dag); + + it('nodes in graph should have expected edges', () => { + expect(nodeA.inputEdges).toHaveLength(7); + expect(nodeA.outputEdges).toHaveLength(0); + expect(nodeA.edges).toHaveLength(7); + + expect(nodeB.inputEdges).toHaveLength(5); + expect(nodeB.outputEdges).toHaveLength(1); + expect(nodeB.edges).toHaveLength(6); + + expect(nodeC.inputEdges).toHaveLength(4); + expect(nodeC.outputEdges).toHaveLength(2); + expect(nodeC.edges).toHaveLength(6); + + expect(nodeD.inputEdges).toHaveLength(2); + expect(nodeD.outputEdges).toHaveLength(3); + expect(nodeD.edges).toHaveLength(5); + + expect(nodeE.inputEdges).toHaveLength(2); + expect(nodeE.outputEdges).toHaveLength(4); + expect(nodeE.edges).toHaveLength(6); + + expect(nodeF.inputEdges).toHaveLength(0); + expect(nodeF.outputEdges).toHaveLength(5); + expect(nodeF.edges).toHaveLength(5); + + expect(nodeG.inputEdges).toHaveLength(0); + expect(nodeG.outputEdges).toHaveLength(3); + expect(nodeG.edges).toHaveLength(3); + + expect(nodeH.inputEdges).toHaveLength(0); + expect(nodeH.outputEdges).toHaveLength(1); + expect(nodeH.edges).toHaveLength(1); + + expect(nodeI.inputEdges).toHaveLength(0); + expect(nodeI.outputEdges).toHaveLength(1); + expect(nodeI.edges).toHaveLength(1); + + expect(nodeA.getEdgeFrom(nodeB)).not.toBeUndefined(); + expect(nodeB.getEdgeTo(nodeA)).not.toBeUndefined(); + }); + + it('when optimizing input edges for node A should return node B and H', () => { + const actual = nodeA.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeB, nodeH])); + }); + + it('when optimizing input edges for node B should return node C', () => { + const actual = nodeB.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeC, nodeI])); + }); + + it('when optimizing input edges for node C should return node D', () => { + const actual = nodeC.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(1); + expect(actual).toEqual(expect.arrayContaining([nodeD])); + }); + + it('when optimizing input edges for node D should return node E', () => { + const actual = nodeD.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(1); + expect(actual).toEqual(expect.arrayContaining([nodeE])); + }); + + it('when optimizing input edges for node E should return node F and G', () => { + const actual = nodeE.getOptimizedInputEdges().map(e => e.inputNode); + expect(actual).toHaveLength(2); + expect(actual).toEqual(expect.arrayContaining([nodeF, nodeG])); + }); + + it('when optimizing input edges for node F should return zero nodes', () => { + const actual = nodeF.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + + it('when optimizing input edges for node G should return zero nodes', () => { + const actual = nodeG.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + + it('when optimizing input edges for node H should return zero nodes', () => { + const actual = nodeH.getOptimizedInputEdges(); + expect(actual).toHaveLength(0); + }); + }); +}); diff --git a/public/app/core/utils/dag.ts b/public/app/core/utils/dag.ts new file mode 100644 index 00000000000..eb7ff1c3b1a --- /dev/null +++ b/public/app/core/utils/dag.ts @@ -0,0 +1,201 @@ +export class Edge { + inputNode: Node; + outputNode: Node; + + _linkTo(node, direction) { + if (direction <= 0) { + node.inputEdges.push(this); + } + + if (direction >= 0) { + node.outputEdges.push(this); + } + + node.edges.push(this); + } + + link(inputNode: Node, outputNode: Node) { + this.unlink(); + this.inputNode = inputNode; + this.outputNode = outputNode; + + this._linkTo(inputNode, 1); + this._linkTo(outputNode, -1); + return this; + } + + unlink() { + let pos; + const inode = this.inputNode; + const onode = this.outputNode; + + if (!(inode && onode)) { + return; + } + + pos = inode.edges.indexOf(this); + if (pos > -1) { + inode.edges.splice(pos, 1); + } + + pos = onode.edges.indexOf(this); + if (pos > -1) { + onode.edges.splice(pos, 1); + } + + pos = inode.outputEdges.indexOf(this); + if (pos > -1) { + inode.outputEdges.splice(pos, 1); + } + + pos = onode.inputEdges.indexOf(this); + if (pos > -1) { + onode.inputEdges.splice(pos, 1); + } + + this.inputNode = null; + this.outputNode = null; + } +} + +export class Node { + name: string; + edges: Edge[]; + inputEdges: Edge[]; + outputEdges: Edge[]; + + constructor(name: string) { + this.name = name; + this.edges = []; + this.inputEdges = []; + this.outputEdges = []; + } + + getEdgeFrom(from: string | Node): Edge { + if (!from) { + return null; + } + + if (typeof from === 'object') { + return this.inputEdges.find(e => e.inputNode.name === from.name); + } + + return this.inputEdges.find(e => e.inputNode.name === from); + } + + getEdgeTo(to: string | Node): Edge { + if (!to) { + return null; + } + + if (typeof to === 'object') { + return this.outputEdges.find(e => e.outputNode.name === to.name); + } + + return this.outputEdges.find(e => e.outputNode.name === to); + } + + getOptimizedInputEdges(): Edge[] { + const toBeRemoved = []; + this.inputEdges.forEach(e => { + const inputEdgesNodes = e.inputNode.inputEdges.map(e => e.inputNode); + + inputEdgesNodes.forEach(n => { + const edgeToRemove = n.getEdgeTo(this.name); + if (edgeToRemove) { + toBeRemoved.push(edgeToRemove); + } + }); + }); + + return this.inputEdges.filter(e => toBeRemoved.indexOf(e) === -1); + } +} + +export class Graph { + nodes = {}; + + constructor() {} + + createNode(name: string): Node { + const n = new Node(name); + this.nodes[name] = n; + return n; + } + + createNodes(names: string[]): Node[] { + const nodes = []; + names.forEach(name => { + nodes.push(this.createNode(name)); + }); + return nodes; + } + + link(input: string | string[] | Node | Node[], output: string | string[] | Node | Node[]): Edge[] { + let inputArr = []; + let outputArr = []; + const inputNodes = []; + const outputNodes = []; + + if (input instanceof Array) { + inputArr = input; + } else { + inputArr = [input]; + } + + if (output instanceof Array) { + outputArr = output; + } else { + outputArr = [output]; + } + + for (let n = 0; n < inputArr.length; n++) { + const i = inputArr[n]; + if (typeof i === 'string') { + inputNodes.push(this.getNode(i)); + } else { + inputNodes.push(i); + } + } + + for (let n = 0; n < outputArr.length; n++) { + const i = outputArr[n]; + if (typeof i === 'string') { + outputNodes.push(this.getNode(i)); + } else { + outputNodes.push(i); + } + } + + const edges = []; + inputNodes.forEach(input => { + outputNodes.forEach(output => { + edges.push(this.createEdge().link(input, output)); + }); + }); + return edges; + } + + createEdge(): Edge { + return new Edge(); + } + + getNode(name: string): Node { + return this.nodes[name]; + } +} + +export const printGraph = (g: Graph) => { + Object.keys(g.nodes).forEach(name => { + const n = g.nodes[name]; + let outputEdges = n.outputEdges.map(e => e.outputNode.name).join(', '); + if (!outputEdges) { + outputEdges = ''; + } + let inputEdges = n.inputEdges.map(e => e.inputNode.name).join(', '); + if (!inputEdges) { + inputEdges = ''; + } + console.log(`${n.name}:\n - links to: ${outputEdges}\n - links from: ${inputEdges}`); + }); +}; diff --git a/public/app/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index f25d340a0be..298a06c64fd 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -74,7 +74,7 @@ export function convertSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATE } export function exportSeriesListToCsv(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsv(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -115,7 +115,7 @@ export function convertSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAU function mergeSeriesByTime(seriesList) { let timestamps = []; for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; + const seriesPoints = seriesList[i].datapoints; for (let j = 0; j < seriesPoints.length; j++) { timestamps.push(seriesPoints[j][POINT_TIME_INDEX]); } @@ -123,9 +123,9 @@ function mergeSeriesByTime(seriesList) { timestamps = sortedUniq(timestamps.sort()); for (let i = 0; i < seriesList.length; i++) { - let seriesPoints = seriesList[i].datapoints; - let seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); - let extendedSeries = []; + const seriesPoints = seriesList[i].datapoints; + const seriesTimestamps = seriesPoints.map(p => p[POINT_TIME_INDEX]); + const extendedSeries = []; let pointIndex; for (let j = 0; j < timestamps.length; j++) { pointIndex = sortedIndexOf(seriesTimestamps, timestamps[j]); @@ -141,7 +141,7 @@ function mergeSeriesByTime(seriesList) { } export function exportSeriesListToCsvColumns(seriesList, dateTimeFormat = DEFAULT_DATETIME_FORMAT, excel = false) { - let text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); + const text = convertSeriesListToCsvColumns(seriesList, dateTimeFormat, excel); saveSaveBlob(text, EXPORT_FILENAME); } @@ -157,11 +157,11 @@ export function convertTableDataToCsv(table, excel = false) { } export function exportTableDataToCsv(table, excel = false) { - let text = convertTableDataToCsv(table, excel); + const text = convertTableDataToCsv(table, excel); saveSaveBlob(text, EXPORT_FILENAME); } export function saveSaveBlob(payload, fname) { - let blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); + const blob = new Blob([payload], { type: 'text/csv;charset=utf-8;header=present;' }); saveAs(blob, fname); } diff --git a/public/app/core/utils/flatten.ts b/public/app/core/utils/flatten.ts index 150017e34f8..3350f5f6c33 100644 --- a/public/app/core/utils/flatten.ts +++ b/public/app/core/utils/flatten.ts @@ -4,19 +4,19 @@ export default function flatten(target, opts): any { opts = opts || {}; - var delimiter = opts.delimiter || '.'; - var maxDepth = opts.maxDepth || 3; - var currentDepth = 1; - var output = {}; + const delimiter = opts.delimiter || '.'; + let maxDepth = opts.maxDepth || 3; + let currentDepth = 1; + const output = {}; function step(object, prev) { Object.keys(object).forEach(function(key) { - var value = object[key]; - var isarray = opts.safe && Array.isArray(value); - var type = Object.prototype.toString.call(value); - var isobject = type === '[object Object]'; + const value = object[key]; + const isarray = opts.safe && Array.isArray(value); + const type = Object.prototype.toString.call(value); + const isobject = type === '[object Object]'; - var newKey = prev ? prev + delimiter + key : key; + const newKey = prev ? prev + delimiter + key : key; if (!opts.maxDepth) { maxDepth = currentDepth + 1; diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 4302e62e3e0..9f30972bc61 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import moment from 'moment'; -var kbn: any = {}; +const kbn: any = {}; kbn.valueFormats = {}; @@ -103,27 +103,27 @@ kbn.round_interval = function(interval) { }; kbn.secondsToHms = function(seconds) { - var numyears = Math.floor(seconds / 31536000); + const numyears = Math.floor(seconds / 31536000); if (numyears) { return numyears + 'y'; } - var numdays = Math.floor((seconds % 31536000) / 86400); + const numdays = Math.floor((seconds % 31536000) / 86400); if (numdays) { return numdays + 'd'; } - var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600); + const numhours = Math.floor(((seconds % 31536000) % 86400) / 3600); if (numhours) { return numhours + 'h'; } - var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); + const numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60); if (numminutes) { return numminutes + 'm'; } - var numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60); + const numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60); if (numseconds) { return numseconds + 's'; } - var nummilliseconds = Math.floor(seconds * 1000.0); + const nummilliseconds = Math.floor(seconds * 1000.0); if (nummilliseconds) { return nummilliseconds + 'ms'; } @@ -132,10 +132,10 @@ kbn.secondsToHms = function(seconds) { }; kbn.secondsToHhmmss = function(seconds) { - var strings = []; - var numhours = Math.floor(seconds / 3600); - var numminutes = Math.floor((seconds % 3600) / 60); - var numseconds = Math.floor((seconds % 3600) % 60); + const strings = []; + const numhours = Math.floor(seconds / 3600); + const numminutes = Math.floor((seconds % 3600) / 60); + const numseconds = Math.floor((seconds % 3600) % 60); numhours > 9 ? strings.push('' + numhours) : strings.push('0' + numhours); numminutes > 9 ? strings.push('' + numminutes) : strings.push('0' + numminutes); numseconds > 9 ? strings.push('' + numseconds) : strings.push('0' + numseconds); @@ -191,7 +191,7 @@ kbn.calculateInterval = function(range, resolution, lowLimitInterval) { }; kbn.describe_interval = function(str) { - var matches = str.match(kbn.interval_regex); + const matches = str.match(kbn.interval_regex); if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) { throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"'); } else { @@ -204,12 +204,12 @@ kbn.describe_interval = function(str) { }; kbn.interval_to_ms = function(str) { - var info = kbn.describe_interval(str); + const info = kbn.describe_interval(str); return info.sec * 1000 * info.count; }; kbn.interval_to_seconds = function(str) { - var info = kbn.describe_interval(str); + const info = kbn.describe_interval(str); return info.sec * info.count; }; @@ -233,7 +233,7 @@ kbn.stringToJsRegex = function(str) { return new RegExp('^' + str + '$'); } - var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); + const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); return new RegExp(match[1], match[2]); }; @@ -242,8 +242,8 @@ kbn.toFixed = function(value, decimals) { return ''; } - var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; - var formatted = String(Math.round(value * factor) / factor); + const factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1; + const formatted = String(Math.round(value * factor) / factor); // if exponent return directly if (formatted.indexOf('e') !== -1 || value === 0) { @@ -253,8 +253,8 @@ kbn.toFixed = function(value, decimals) { // If tickDecimals was specified, ensure that we have exactly that // much precision; otherwise default to the value's own precision. if (decimals != null) { - var decimalPos = formatted.indexOf('.'); - var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; + const decimalPos = formatted.indexOf('.'); + const precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1; if (precision < decimals) { return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision); } @@ -275,8 +275,8 @@ kbn.roundValue = function(num, decimals) { if (num === null) { return null; } - var n = Math.pow(10, decimals); - var formatted = (n * num).toFixed(decimals); + const n = Math.pow(10, decimals); + const formatted = (n * num).toFixed(decimals); return Math.round(parseFloat(formatted)) / n; }; @@ -305,7 +305,7 @@ kbn.formatBuilders.scaledUnits = function(factor, extArray) { } var steps = 0; - var limit = extArray.length; + const limit = extArray.length; while (Math.abs(size) >= factor) { steps++; @@ -330,7 +330,7 @@ kbn.formatBuilders.scaledUnits = function(factor, extArray) { kbn.formatBuilders.decimalSIPrefix = function(unit, offset) { var prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y']; prefixes = prefixes.slice(3 + (offset || 0)); - var units = prefixes.map(function(p) { + const units = prefixes.map(function(p) { return ' ' + p + unit; }); return kbn.formatBuilders.scaledUnits(1000, units); @@ -340,8 +340,8 @@ kbn.formatBuilders.decimalSIPrefix = function(unit, offset) { // offset is given, it starts the units at the given prefix; otherwise, the // offset defaults to zero and the initial unit is not prefixed. kbn.formatBuilders.binarySIPrefix = function(unit, offset) { - var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); - var units = prefixes.map(function(p) { + const prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset); + const units = prefixes.map(function(p) { return ' ' + p + unit; }); return kbn.formatBuilders.scaledUnits(1024, units); @@ -350,25 +350,25 @@ kbn.formatBuilders.binarySIPrefix = function(unit, offset) { // Currency formatter for prefixing a symbol onto a number. Supports scaling // up to the trillions. kbn.formatBuilders.currency = function(symbol) { - var units = ['', 'K', 'M', 'B', 'T']; - var scaler = kbn.formatBuilders.scaledUnits(1000, units); + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = kbn.formatBuilders.scaledUnits(1000, units); return function(size, decimals, scaledDecimals) { if (size === null) { return ''; } - var scaled = scaler(size, decimals, scaledDecimals); + const scaled = scaler(size, decimals, scaledDecimals); return symbol + scaled; }; }; kbn.formatBuilders.simpleCountUnit = function(symbol) { - var units = ['', 'K', 'M', 'B', 'T']; - var scaler = kbn.formatBuilders.scaledUnits(1000, units); + const units = ['', 'K', 'M', 'B', 'T']; + const scaler = kbn.formatBuilders.scaledUnits(1000, units); return function(size, decimals, scaledDecimals) { if (size === null) { return ''; } - var scaled = scaler(size, decimals, scaledDecimals); + const scaled = scaler(size, decimals, scaledDecimals); return scaled + ' ' + symbol; }; }; @@ -420,7 +420,7 @@ kbn.valueFormats.hex0x = function(value, decimals) { if (value == null) { return ''; } - var hexString = kbn.valueFormats.hex(value, decimals); + const hexString = kbn.valueFormats.hex(value, decimals); if (hexString.substring(0, 1) === '-') { return '-0x' + hexString.substring(1); } @@ -449,6 +449,7 @@ kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr'); kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr'); kbn.valueFormats.currencyCZK = kbn.formatBuilders.currency('czk'); kbn.valueFormats.currencyCHF = kbn.formatBuilders.currency('CHF'); +kbn.valueFormats.currencyPLN = kbn.formatBuilders.currency('zł'); // Data (Binary) kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b'); @@ -499,7 +500,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W'); kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1); kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1); kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1); -kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2'); +kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m²'); kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA'); kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1); kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var'); @@ -571,9 +572,9 @@ kbn.valueFormats.accG = kbn.formatBuilders.fixedUnit('g'); // Volume kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L'); kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1); -kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m3'); -kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm3'); -kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm3'); +kbn.valueFormats.m3 = kbn.formatBuilders.fixedUnit('m³'); +kbn.valueFormats.Nm3 = kbn.formatBuilders.fixedUnit('Nm³'); +kbn.valueFormats.dm3 = kbn.formatBuilders.fixedUnit('dm³'); kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal'); // Flow @@ -604,14 +605,14 @@ kbn.valueFormats.radsvh = kbn.formatBuilders.decimalSIPrefix('Sv/h'); // Concentration kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm'); kbn.valueFormats.conppb = kbn.formatBuilders.fixedUnit('ppb'); -kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m3'); -kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm3'); -kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m3'); -kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm3'); -kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m3'); -kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm3'); -kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m3'); -kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm3'); +kbn.valueFormats.conngm3 = kbn.formatBuilders.fixedUnit('ng/m³'); +kbn.valueFormats.conngNm3 = kbn.formatBuilders.fixedUnit('ng/Nm³'); +kbn.valueFormats.conμgm3 = kbn.formatBuilders.fixedUnit('μg/m³'); +kbn.valueFormats.conμgNm3 = kbn.formatBuilders.fixedUnit('μg/Nm³'); +kbn.valueFormats.conmgm3 = kbn.formatBuilders.fixedUnit('mg/m³'); +kbn.valueFormats.conmgNm3 = kbn.formatBuilders.fixedUnit('mg/Nm³'); +kbn.valueFormats.congm3 = kbn.formatBuilders.fixedUnit('g/m³'); +kbn.valueFormats.congNm3 = kbn.formatBuilders.fixedUnit('g/Nm³'); // Time kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz'); @@ -768,7 +769,7 @@ kbn.toDuration = function(size, decimals, timeScale) { return kbn.toDuration(-size, decimals, timeScale) + ' ago'; } - var units = [ + const units = [ { short: 'y', long: 'year' }, { short: 'M', long: 'month' }, { short: 'w', long: 'week' }, @@ -787,16 +788,16 @@ kbn.toDuration = function(size, decimals, timeScale) { }).short ] * 1000; - var strings = []; + const strings = []; // after first value >= 1 print only $decimals more var decrementDecimals = false; for (var i = 0; i < units.length && decimals >= 0; i++) { - var interval = kbn.intervals_in_seconds[units[i].short] * 1000; - var value = size / interval; + const interval = kbn.intervals_in_seconds[units[i].short] * 1000; + const value = size / interval; if (value >= 1 || decrementDecimals) { decrementDecimals = true; - var floor = Math.floor(value); - var unit = units[i].long + (floor !== 1 ? 's' : ''); + const floor = Math.floor(value); + const unit = units[i].long + (floor !== 1 ? 's' : ''); strings.push(floor + ' ' + unit); size = size % interval; decimals--; @@ -823,7 +824,7 @@ kbn.valueFormats.timeticks = function(size, decimals, scaledDecimals) { }; kbn.valueFormats.dateTimeAsIso = function(epoch, isUtc) { - var time = isUtc ? moment.utc(epoch) : moment(epoch); + const time = isUtc ? moment.utc(epoch) : moment(epoch); if (moment().isSame(epoch, 'day')) { return time.format('HH:mm:ss'); @@ -832,7 +833,7 @@ kbn.valueFormats.dateTimeAsIso = function(epoch, isUtc) { }; kbn.valueFormats.dateTimeAsUS = function(epoch, isUtc) { - var time = isUtc ? moment.utc(epoch) : moment(epoch); + const time = isUtc ? moment.utc(epoch) : moment(epoch); if (moment().isSame(epoch, 'day')) { return time.format('h:mm:ss a'); @@ -841,7 +842,7 @@ kbn.valueFormats.dateTimeAsUS = function(epoch, isUtc) { }; kbn.valueFormats.dateTimeFromNow = function(epoch, isUtc) { - var time = isUtc ? moment.utc(epoch) : moment(epoch); + const time = isUtc ? moment.utc(epoch) : moment(epoch); return time.fromNow(); }; @@ -880,6 +881,7 @@ kbn.getUnitFormats = function() { { text: 'Swedish Krona (kr)', value: 'currencySEK' }, { text: 'Czech koruna (czk)', value: 'currencyCZK' }, { text: 'Swiss franc (CHF)', value: 'currencyCHF' }, + { text: 'Polish Złoty (PLN)', value: 'currencyPLN' }, ], }, { @@ -957,7 +959,7 @@ kbn.getUnitFormats = function() { text: 'throughput', submenu: [ { text: 'ops/sec (ops)', value: 'ops' }, - { text: 'requets/sec (rps)', value: 'reqps' }, + { text: 'requests/sec (rps)', value: 'reqps' }, { text: 'reads/sec (rps)', value: 'rps' }, { text: 'writes/sec (wps)', value: 'wps' }, { text: 'I/O ops/sec (iops)', value: 'iops' }, @@ -1019,7 +1021,7 @@ kbn.getUnitFormats = function() { { text: 'Watt (W)', value: 'watt' }, { text: 'Kilowatt (kW)', value: 'kwatt' }, { text: 'Milliwatt (mW)', value: 'mwatt' }, - { text: 'Watt per square metre (W/m2)', value: 'Wm2' }, + { text: 'Watt per square metre (W/m²)', value: 'Wm2' }, { text: 'Volt-ampere (VA)', value: 'voltamp' }, { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' }, { text: 'Volt-ampere reactive (var)', value: 'voltampreact' }, @@ -1116,14 +1118,14 @@ kbn.getUnitFormats = function() { submenu: [ { text: 'parts-per-million (ppm)', value: 'ppm' }, { text: 'parts-per-billion (ppb)', value: 'conppb' }, - { text: 'nanogram per cubic metre (ng/m3)', value: 'conngm3' }, - { text: 'nanogram per normal cubic metre (ng/Nm3)', value: 'conngNm3' }, - { text: 'microgram per cubic metre (μg/m3)', value: 'conμgm3' }, - { text: 'microgram per normal cubic metre (μg/Nm3)', value: 'conμgNm3' }, - { text: 'milligram per cubic metre (mg/m3)', value: 'conmgm3' }, - { text: 'milligram per normal cubic metre (mg/Nm3)', value: 'conmgNm3' }, - { text: 'gram per cubic metre (g/m3)', value: 'congm3' }, - { text: 'gram per normal cubic metre (g/Nm3)', value: 'congNm3' }, + { text: 'nanogram per cubic metre (ng/m³)', value: 'conngm3' }, + { text: 'nanogram per normal cubic metre (ng/Nm³)', value: 'conngNm3' }, + { text: 'microgram per cubic metre (μg/m³)', value: 'conμgm3' }, + { text: 'microgram per normal cubic metre (μg/Nm³)', value: 'conμgNm3' }, + { text: 'milligram per cubic metre (mg/m³)', value: 'conmgm3' }, + { text: 'milligram per normal cubic metre (mg/Nm³)', value: 'conmgNm3' }, + { text: 'gram per cubic metre (g/m³)', value: 'congm3' }, + { text: 'gram per normal cubic metre (g/Nm³)', value: 'congNm3' }, ], }, ]; diff --git a/public/app/core/utils/outline.ts b/public/app/core/utils/outline.ts index 94393e781e9..cc06102bfdc 100644 --- a/public/app/core/utils/outline.ts +++ b/public/app/core/utils/outline.ts @@ -1,6 +1,6 @@ // based on http://www.paciellogroup.com/blog/2012/04/how-to-remove-css-outlines-in-an-accessible-manner/ function outlineFixer() { - let d: any = document; + const d: any = document; var style_element = d.createElement('STYLE'); var dom_events = 'addEventListener' in d; diff --git a/public/app/core/utils/rangeutil.ts b/public/app/core/utils/rangeutil.ts index 95cfe42f0b8..bda861e4b9f 100644 --- a/public/app/core/utils/rangeutil.ts +++ b/public/app/core/utils/rangeutil.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import moment from 'moment'; import * as dateMath from './datemath'; -var spans = { +const spans = { s: { display: 'second' }, m: { display: 'minute' }, h: { display: 'hour' }, @@ -12,7 +12,7 @@ var spans = { y: { display: 'year' }, }; -var rangeOptions = [ +const rangeOptions = [ { from: 'now/d', to: 'now/d', display: 'Today', section: 2 }, { from: 'now/d', to: 'now', display: 'Today so far', section: 2 }, { from: 'now/w', to: 'now/w', display: 'This week', section: 2 }, @@ -58,15 +58,15 @@ var rangeOptions = [ { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 }, ]; -var absoluteFormat = 'MMM D, YYYY HH:mm:ss'; +const absoluteFormat = 'MMM D, YYYY HH:mm:ss'; -var rangeIndex = {}; +const rangeIndex = {}; _.each(rangeOptions, function(frame) { rangeIndex[frame.from + ' to ' + frame.to] = frame; }); export function getRelativeTimesList(timepickerSettings, currentDisplay) { - var groups = _.groupBy(rangeOptions, (option: any) => { + const groups = _.groupBy(rangeOptions, (option: any) => { option.active = option.display === currentDisplay; return option.section; }); @@ -92,7 +92,7 @@ function formatDate(date) { // now/d // if no to then to now is assumed export function describeTextRange(expr: any) { - let isLast = expr.indexOf('+') !== 0; + const isLast = expr.indexOf('+') !== 0; if (expr.indexOf('now') === -1) { expr = (isLast ? 'now-' : 'now') + expr; } @@ -108,11 +108,11 @@ export function describeTextRange(expr: any) { opt = { from: 'now', to: expr }; } - let parts = /^now([-+])(\d+)(\w)/.exec(expr); + const parts = /^now([-+])(\d+)(\w)/.exec(expr); if (parts) { - let unit = parts[3]; - let amount = parseInt(parts[2]); - let span = spans[unit]; + const unit = parts[3]; + const amount = parseInt(parts[2]); + const span = spans[unit]; if (span) { opt.display = isLast ? 'Last ' : 'Next '; opt.display += amount + ' ' + span.display; @@ -130,7 +130,7 @@ export function describeTextRange(expr: any) { } export function describeTimeRange(range) { - var option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()]; + const option = rangeIndex[range.from.toString() + ' to ' + range.to.toString()]; if (option) { return option.display; } @@ -140,17 +140,17 @@ export function describeTimeRange(range) { } if (moment.isMoment(range.from)) { - var toMoment = dateMath.parse(range.to, true); + const toMoment = dateMath.parse(range.to, true); return formatDate(range.from) + ' to ' + toMoment.fromNow(); } if (moment.isMoment(range.to)) { - var from = dateMath.parse(range.from, false); + const from = dateMath.parse(range.from, false); return from.fromNow() + ' to ' + formatDate(range.to); } if (range.to.toString() === 'now') { - var res = describeTextRange(range.from); + const res = describeTextRange(range.from); return res.display; } diff --git a/public/app/core/utils/sort_by_keys.ts b/public/app/core/utils/sort_by_keys.ts index 9dff252576a..0020d04f290 100644 --- a/public/app/core/utils/sort_by_keys.ts +++ b/public/app/core/utils/sort_by_keys.ts @@ -7,7 +7,7 @@ export default function sortByKeys(input) { if (_.isPlainObject(input)) { var sortedObject = {}; - for (let key of _.keys(input).sort()) { + for (const key of _.keys(input).sort()) { sortedObject[key] = sortByKeys(input[key]); } return sortedObject; diff --git a/public/app/core/utils/tags.ts b/public/app/core/utils/tags.ts index 678fd8c94be..d0f244be76b 100644 --- a/public/app/core/utils/tags.ts +++ b/public/app/core/utils/tags.ts @@ -67,9 +67,9 @@ const TAG_BORDER_COLORS = [ * @param name tag name */ export function getTagColorsFromName(name: string): { color: string; borderColor: string } { - let hash = djb2(name.toLowerCase()); - let color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; - let borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; + const hash = djb2(name.toLowerCase()); + const color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)]; + const borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)]; return { color, borderColor }; } diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 66e6a7ce4fc..d87dedccab1 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -7,7 +7,7 @@ * @param count Ticks count */ export function tickStep(start: number, stop: number, count: number): number { - let e10 = Math.sqrt(50), + const e10 = Math.sqrt(50), e5 = Math.sqrt(10), e2 = Math.sqrt(2); @@ -76,7 +76,7 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) { let min = +(panelMin != null ? panelMin : datamin); let max = +(panelMax != null ? panelMax : datamax); - let delta = max - min; + const delta = max - min; if (delta === 0.0) { // Grafana fix: wide Y min and max using increased wideFactor @@ -123,11 +123,11 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { const { min, max } = getFlotRange(axis.min, axis.max, datamin, datamax); const noTicks = 0.3 * Math.sqrt(height); const delta = (max - min) / noTicks; - let dec = -Math.floor(Math.log(delta) / Math.LN10); + const dec = -Math.floor(Math.log(delta) / Math.LN10); - let magn = Math.pow(10, -dec); + const magn = Math.pow(10, -dec); // norm is between 1.0 and 10.0 - let norm = delta / magn; + const norm = delta / magn; let size; if (norm < 1.5) { @@ -159,10 +159,10 @@ export function getFlotTickDecimals(datamin, datamax, axis, height) { */ export function grafanaTimeFormat(ticks, min, max) { if (min && max && ticks) { - let range = max - min; - let secPerTick = range / ticks / 1000; - let oneDay = 86400000; - let oneYear = 31536000000; + const range = max - min; + const secPerTick = range / ticks / 1000; + const oneDay = 86400000; + const oneYear = 31536000000; if (secPerTick <= 45) { return '%H:%M:%S'; @@ -193,7 +193,7 @@ export function logp(value, base) { * Get decimal precision of number (3.14 => 2) */ export function getPrecision(num: number): number { - let str = num.toString(); + const str = num.toString(); return getStringPrecision(str); } @@ -201,7 +201,7 @@ export function getPrecision(num: number): number { * Get decimal precision of number stored as a string ("3.14" => 2) */ export function getStringPrecision(num: string): number { - let dot_index = num.indexOf('.'); + const dot_index = num.indexOf('.'); if (dot_index === -1) { return 0; } else { diff --git a/public/app/core/utils/url.ts b/public/app/core/utils/url.ts index b57d5721d57..857e76d9094 100644 --- a/public/app/core/utils/url.ts +++ b/public/app/core/utils/url.ts @@ -3,14 +3,14 @@ */ export function toUrlParams(a) { - let s = []; - let rbracket = /\[\]$/; + const s = []; + const rbracket = /\[\]$/; - let isArray = function(obj) { + const isArray = function(obj) { return Object.prototype.toString.call(obj) === '[object Array]'; }; - let add = function(k, v) { + const add = function(k, v) { v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; if (typeof v !== 'boolean') { s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v); @@ -19,7 +19,7 @@ export function toUrlParams(a) { } }; - let buildParams = function(prefix, obj) { + const buildParams = function(prefix, obj) { var i, len, key; if (prefix) { diff --git a/public/app/core/utils/version.ts b/public/app/core/utils/version.ts index 6ee1400df51..8b249563d86 100644 --- a/public/app/core/utils/version.ts +++ b/public/app/core/utils/version.ts @@ -9,7 +9,7 @@ export class SemVersion { meta: string; constructor(version: string) { - let match = versionPattern.exec(version); + const match = versionPattern.exec(version); if (match) { this.major = Number(match[1]); this.minor = Number(match[2] || 0); @@ -19,7 +19,7 @@ export class SemVersion { } isGtOrEq(version: string): boolean { - let compared = new SemVersion(version); + const compared = new SemVersion(version); return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); } @@ -29,6 +29,6 @@ export class SemVersion { } export function isVersionGtOrEq(a: string, b: string): boolean { - let a_semver = new SemVersion(a); + const a_semver = new SemVersion(a); return a_semver.isGtOrEq(b); } diff --git a/public/app/features/admin/admin_edit_user_ctrl.ts b/public/app/features/admin/admin_edit_user_ctrl.ts index 1d4fb9cf19a..b84b690d44a 100644 --- a/public/app/features/admin/admin_edit_user_ctrl.ts +++ b/public/app/features/admin/admin_edit_user_ctrl.ts @@ -29,14 +29,14 @@ export class AdminEditUserCtrl { return; } - var payload = { password: $scope.password }; + const payload = { password: $scope.password }; backendSrv.put('/api/admin/users/' + $scope.user_id + '/password', payload).then(function() { $location.path('/admin/users'); }); }; $scope.updatePermissions = function() { - var payload = $scope.permissions; + const payload = $scope.permissions; backendSrv.put('/api/admin/users/' + $scope.user_id + '/permissions', payload).then(function() { $location.path('/admin/users'); @@ -99,7 +99,7 @@ export class AdminEditUserCtrl { return; } - var orgInfo = _.find($scope.orgsSearchCache, { + const orgInfo = _.find($scope.orgsSearchCache, { name: $scope.newOrg.name, }); if (!orgInfo) { diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 79baa1e3f5a..a25d37913d4 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -184,7 +184,7 @@ export class AlertTabCtrl { ThresholdMapper.alertToGraphThresholds(this.panel); - for (let addedNotification of alert.notifications) { + for (const addedNotification of alert.notifications) { var model = _.find(this.notifications, { id: addedNotification.id }); if (model && model.isDefault === false) { model.iconClass = this.getNotificationIcon(model.type); @@ -192,7 +192,7 @@ export class AlertTabCtrl { } } - for (let notification of this.notifications) { + for (const notification of this.notifications) { if (notification.isDefault) { notification.iconClass = this.getNotificationIcon(notification.type); notification.bgColor = '#00678b'; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..60942e6ffb4 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -30,7 +30,7 @@ export class AlertNotificationEditCtrl { this.notifiers = notifiers; // add option templates - for (let notifier of this.notifiers) { + for (const notifier of this.notifiers) { this.$templateCache.put(this.getNotifierTemplateId(notifier.type), notifier.optionsTemplate); } @@ -99,7 +99,7 @@ export class AlertNotificationEditCtrl { return; } - var payload = { + const payload = { name: this.model.name, type: this.model.type, settings: this.model.settings, diff --git a/public/app/features/alerting/specs/alert_tab_specs.ts b/public/app/features/alerting/specs/alert_tab_specs.ts deleted file mode 100644 index 4a4de34fe6c..00000000000 --- a/public/app/features/alerting/specs/alert_tab_specs.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { describe, it, expect } from 'test/lib/common'; - -import { AlertTabCtrl } from '../alert_tab_ctrl'; - -describe('AlertTabCtrl', () => { - var $scope = { - ctrl: {}, - }; - - describe('with null parameters', () => { - it('can be created', () => { - var alertTab = new AlertTabCtrl($scope, null, null, null, null, null); - - expect(alertTab).to.not.be(null); - }); - }); -}); diff --git a/public/app/features/alerting/specs/threshold_mapper.jest.ts b/public/app/features/alerting/specs/threshold_mapper.test.ts similarity index 86% rename from public/app/features/alerting/specs/threshold_mapper.jest.ts rename to public/app/features/alerting/specs/threshold_mapper.test.ts index b9fa45a6e49..922d9c8787e 100644 --- a/public/app/features/alerting/specs/threshold_mapper.jest.ts +++ b/public/app/features/alerting/specs/threshold_mapper.test.ts @@ -5,7 +5,7 @@ import { ThresholdMapper } from '../threshold_mapper'; describe('ThresholdMapper', () => { describe('with greater than evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -17,7 +17,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); @@ -26,7 +26,7 @@ describe('ThresholdMapper', () => { describe('with outside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -38,7 +38,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('lt'); expect(panel.thresholds[0].value).toBe(100); @@ -50,7 +50,7 @@ describe('ThresholdMapper', () => { describe('with inside range evaluator', () => { it('can map query conditions to thresholds', () => { - var panel: any = { + const panel: any = { type: 'graph', alert: { conditions: [ @@ -62,7 +62,7 @@ describe('ThresholdMapper', () => { }, }; - var updated = ThresholdMapper.alertToGraphThresholds(panel); + const updated = ThresholdMapper.alertToGraphThresholds(panel); expect(updated).toBe(true); expect(panel.thresholds[0].op).toBe('gt'); expect(panel.thresholds[0].value).toBe(100); diff --git a/public/app/features/alerting/threshold_mapper.ts b/public/app/features/alerting/threshold_mapper.ts index 9142c74b6e3..50324dc18ca 100644 --- a/public/app/features/alerting/threshold_mapper.ts +++ b/public/app/features/alerting/threshold_mapper.ts @@ -1,7 +1,7 @@ export class ThresholdMapper { static alertToGraphThresholds(panel) { for (var i = 0; i < panel.alert.conditions.length; i++) { - let condition = panel.alert.conditions[i]; + const condition = panel.alert.conditions[i]; if (condition.type !== 'query') { continue; } @@ -11,18 +11,18 @@ export class ThresholdMapper { switch (evaluator.type) { case 'gt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'gt' }); break; } case 'lt': { - let value = evaluator.params[0]; + const value = evaluator.params[0]; thresholds.push({ value: value, op: 'lt' }); break; } case 'outside_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'gt' }); @@ -35,8 +35,8 @@ export class ThresholdMapper { break; } case 'within_range': { - let value1 = evaluator.params[0]; - let value2 = evaluator.params[1]; + const value1 = evaluator.params[0]; + const value2 = evaluator.params[1]; if (value1 > value2) { thresholds.push({ value: value1, op: 'lt' }); diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 5578a979146..b8def36829a 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -91,7 +91,7 @@ export class AnnotationsSrv { var range = this.timeSrv.timeRange(); var promises = []; - for (let annotation of dashboard.annotations.list) { + for (const annotation of dashboard.annotations.list) { if (!annotation.enable) { continue; } diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts index 1f94e978029..90c425438ab 100644 --- a/public/app/features/annotations/event_editor.ts +++ b/public/app/features/annotations/event_editor.ts @@ -31,7 +31,7 @@ export class EventEditorCtrl { return; } - let saveModel = _.cloneDeep(this.event); + const saveModel = _.cloneDeep(this.event); saveModel.time = saveModel.time.valueOf(); saveModel.timeEnd = 0; @@ -85,7 +85,7 @@ export class EventEditorCtrl { function tryEpochToMoment(timestamp) { if (timestamp && _.isNumber(timestamp)) { - let epoch = Number(timestamp); + const epoch = Number(timestamp); return moment(epoch); } else { return timestamp; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index 7db6a19f2c6..a6fceac2e54 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -125,11 +125,11 @@ export class EventManager { } } - let regions = getRegions(annotations); + const regions = getRegions(annotations); addRegionMarking(regions, flotOptions); - let eventSectionHeight = 20; - let eventSectionMargin = 7; + const eventSectionHeight = 20; + const eventSectionMargin = 7; flotOptions.grid.eventSectionHeight = eventSectionMargin; flotOptions.xaxis.eventSectionHeight = eventSectionHeight; @@ -147,8 +147,8 @@ function getRegions(events) { } function addRegionMarking(regions, flotOptions) { - let markings = flotOptions.grid.markings; - let defaultColor = DEFAULT_ANNOTATION_COLOR; + const markings = flotOptions.grid.markings; + const defaultColor = DEFAULT_ANNOTATION_COLOR; let fillColor; _.each(regions, region => { @@ -167,7 +167,7 @@ function addRegionMarking(regions, flotOptions) { } function addAlphaToRGB(colorString: string, alpha: number): string { - let color = tinycolor(colorString); + const color = tinycolor(colorString); if (color.isValid()) { color.setAlpha(alpha); return color.toRgbString(); diff --git a/public/app/features/annotations/events_processing.ts b/public/app/features/annotations/events_processing.ts index 667285d7d43..6e610fb1457 100644 --- a/public/app/features/annotations/events_processing.ts +++ b/public/app/features/annotations/events_processing.ts @@ -7,20 +7,20 @@ import _ from 'lodash'; * @param options */ export function makeRegions(annotations, options) { - let [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); - let regions = getRegions(regionEvents, options.range); + const [regionEvents, singleEvents] = _.partition(annotations, 'regionId'); + const regions = getRegions(regionEvents, options.range); annotations = _.concat(regions, singleEvents); return annotations; } function getRegions(events, range) { - let region_events = _.filter(events, event => { + const region_events = _.filter(events, event => { return event.regionId; }); let regions = _.groupBy(region_events, 'regionId'); regions = _.compact( _.map(regions, region_events => { - let region_obj = _.head(region_events); + const region_obj = _.head(region_events); if (region_events && region_events.length > 1) { region_obj.timeEnd = region_events[1].time; region_obj.isRegion = true; @@ -57,9 +57,9 @@ export function dedupAnnotations(annotations) { let dedup = []; // Split events by annotationId property existence - let events = _.partition(annotations, 'id'); + const events = _.partition(annotations, 'id'); - let eventsById = _.groupBy(events[0], 'id'); + const eventsById = _.groupBy(events[0], 'id'); dedup = _.map(eventsById, eventGroup => { if (eventGroup.length > 1 && !_.every(eventGroup, isPanelAlert)) { // Get first non-panel alert diff --git a/public/app/features/annotations/specs/annotations_srv.jest.ts b/public/app/features/annotations/specs/annotations_srv.test.ts similarity index 82% rename from public/app/features/annotations/specs/annotations_srv.jest.ts rename to public/app/features/annotations/specs/annotations_srv.test.ts index 7db7b6c9f05..f262544da43 100644 --- a/public/app/features/annotations/specs/annotations_srv.jest.ts +++ b/public/app/features/annotations/specs/annotations_srv.test.ts @@ -3,15 +3,11 @@ import 'app/features/dashboard/time_srv'; import { AnnotationsSrv } from '../annotations_srv'; describe('AnnotationsSrv', function() { - let $rootScope = { + const $rootScope = { onAppEvent: jest.fn(), }; - let $q; - let datasourceSrv; - let backendSrv; - let timeSrv; - let annotationsSrv = new AnnotationsSrv($rootScope, $q, datasourceSrv, backendSrv, timeSrv); + const annotationsSrv = new AnnotationsSrv($rootScope, null, null, null, null); describe('When translating the query result', () => { const annotationSource = { diff --git a/public/app/features/annotations/specs/annotations_srv_specs.jest.ts b/public/app/features/annotations/specs/annotations_srv_specs.test.ts similarity index 88% rename from public/app/features/annotations/specs/annotations_srv_specs.jest.ts rename to public/app/features/annotations/specs/annotations_srv_specs.test.ts index 35def83e4a9..49457f34d05 100644 --- a/public/app/features/annotations/specs/annotations_srv_specs.jest.ts +++ b/public/app/features/annotations/specs/annotations_srv_specs.test.ts @@ -24,7 +24,7 @@ describe('Annotations', () => { { id: 2, time: 2 }, ]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); @@ -33,7 +33,7 @@ describe('Annotations', () => { testAnnotations = [{ id: 5, time: 4, regionId: 5 }]; const expectedAnnotations = [{ id: 5, regionId: 5, isRegion: true, time: 4, timeEnd: 7 }]; - let regions = makeRegions(testAnnotations, { range: range }); + const regions = makeRegions(testAnnotations, { range: range }); expect(regions).toEqual(expectedAnnotations); }); }); @@ -49,7 +49,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); @@ -63,7 +63,7 @@ describe('Annotations', () => { ]; const expectedAnnotations = [{ id: 1, time: 1 }, { id: 2, time: 2 }, { id: 5, time: 5 }]; - let deduplicated = dedupAnnotations(testAnnotations); + const deduplicated = dedupAnnotations(testAnnotations); expect(deduplicated).toEqual(expectedAnnotations); }); }); diff --git a/public/app/features/dashboard/ad_hoc_filters.ts b/public/app/features/dashboard/ad_hoc_filters.ts index ee57db23675..68b068152b5 100644 --- a/public/app/features/dashboard/ad_hoc_filters.ts +++ b/public/app/features/dashboard/ad_hoc_filters.ts @@ -30,7 +30,7 @@ export class AdHocFiltersCtrl { if (this.variable.value && !_.isArray(this.variable.value)) { } - for (let tag of this.variable.filters) { + for (const tag of this.variable.filters) { if (this.segments.length > 0) { this.segments.push(this.uiSegmentSrv.newCondition('AND')); } @@ -55,8 +55,8 @@ export class AdHocFiltersCtrl { } return this.datasourceSrv.get(this.variable.datasource).then(ds => { - var options: any = {}; - var promise = null; + const options: any = {}; + let promise = null; if (segment.type !== 'value') { promise = ds.getTagKeys(); @@ -113,9 +113,9 @@ export class AdHocFiltersCtrl { } updateVariableModel() { - var filters = []; - var filterIndex = -1; - var hasFakes = false; + const filters = []; + let filterIndex = -1; + let hasFakes = false; this.segments.forEach(segment => { if (segment.type === 'value' && segment.fake) { @@ -153,7 +153,7 @@ export class AdHocFiltersCtrl { } } -var template = ` +const template = `
    { + const self = this; + const cancel = this.$rootScope.$on('dashboard-saved', () => { cancel(); this.$timeout(() => { self.gotoNext(); @@ -179,8 +179,8 @@ export class ChangeTracker { } gotoNext() { - var baseLen = this.$location.absUrl().length - this.$location.url().length; - var nextUrl = this.next.substring(baseLen); + const baseLen = this.$location.absUrl().length - this.$location.url().length; + const nextUrl = this.next.substring(baseLen); this.$location.url(nextUrl); } } diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 94d0b18f157..5524b55e3dd 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -62,6 +62,8 @@ export class DashboardCtrl implements PanelContainer { .finally(() => { this.dashboard = dashboard; this.dashboard.processRepeats(); + this.dashboard.updateSubmenuVisibility(); + this.dashboard.autoFitPanels(window.innerHeight); this.unsavedChangesSrv.init(dashboard, this.$scope); @@ -70,8 +72,6 @@ export class DashboardCtrl implements PanelContainer { this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - - this.dashboard.updateSubmenuVisibility(); this.setWindowTitleAndTheme(); this.$scope.appEvent('dashboard-initialized', dashboard); @@ -106,7 +106,7 @@ export class DashboardCtrl implements PanelContainer { } showJsonEditor(evt, options) { - var editScope = this.$rootScope.$new(); + const editScope = this.$rootScope.$new(); editScope.object = options.object; editScope.updateHandler = options.updateHandler; this.$scope.appEvent('show-dash-editor', { @@ -137,7 +137,7 @@ export class DashboardCtrl implements PanelContainer { return; } - var panelInfo = this.dashboard.getPanelInfoById(options.panelId); + const panelInfo = this.dashboard.getPanelInfoById(options.panelId); this.removePanel(panelInfo.panel, true); } diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index 73e9e316b4e..3dfae1250dd 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -51,8 +51,8 @@ export class DashboardImportCtrl { this.inputs = []; if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { - var inputModel = { + for (const input of this.dash.__inputs) { + const inputModel = { name: input.name, label: input.label, info: input.description, @@ -78,7 +78,7 @@ export class DashboardImportCtrl { } setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { + const sources = _.filter(config.datasources, val => { return val.type === input.pluginId; }); @@ -95,7 +95,7 @@ export class DashboardImportCtrl { inputValueChanged() { this.inputsValid = true; - for (let input of this.inputs) { + for (const input of this.inputs) { if (!input.value) { this.inputsValid = false; } @@ -162,7 +162,7 @@ export class DashboardImportCtrl { } saveDashboard() { - var inputs = this.inputs.map(input => { + const inputs = this.inputs.map(input => { return { name: input.name, type: input.type, @@ -186,7 +186,7 @@ export class DashboardImportCtrl { loadJsonText() { try { this.parseError = ''; - var dash = JSON.parse(this.jsonText); + const dash = JSON.parse(this.jsonText); this.onUpload(dash); } catch (err) { console.log(err); @@ -198,8 +198,8 @@ export class DashboardImportCtrl { checkGnetDashboard() { this.gnetError = ''; - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; + const match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + let dashboardId; if (match && match[1]) { dashboardId = match[1]; diff --git a/public/app/features/dashboard/dashboard_loader_srv.ts b/public/app/features/dashboard/dashboard_loader_srv.ts index 9e458c4e4bb..cbf753c0124 100644 --- a/public/app/features/dashboard/dashboard_loader_srv.ts +++ b/public/app/features/dashboard/dashboard_loader_srv.ts @@ -71,7 +71,7 @@ export class DashboardLoaderSrv { } _loadScriptedDashboard(file) { - var url = 'public/dashboards/' + file.replace(/\.(?!js)/, '/') + '?' + new Date().getTime(); + const url = 'public/dashboards/' + file.replace(/\.(?!js)/, '/') + '?' + new Date().getTime(); return this.$http({ url: url, method: 'GET' }) .then(this._executeScript.bind(this)) @@ -99,14 +99,14 @@ export class DashboardLoaderSrv { } _executeScript(result) { - var services = { + const services = { dashboardSrv: this.dashboardSrv, datasourceSrv: this.datasourceSrv, $q: this.$q, }; /*jshint -W054 */ - var script_func = new Function( + const script_func = new Function( 'ARGS', 'kbn', 'dateMath', @@ -119,11 +119,11 @@ export class DashboardLoaderSrv { 'services', result.data ); - var script_result = script_func(this.$routeParams, kbn, dateMath, _, moment, window, document, $, $, services); + const script_result = script_func(this.$routeParams, kbn, dateMath, _, moment, window, document, $, $, services); // Handle async dashboard scripts if (_.isFunction(script_result)) { - var deferred = this.$q.defer(); + const deferred = this.$q.defer(); script_result(dashboard => { this.$timeout(() => { deferred.resolve({ data: dashboard }); diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 1d319929bfd..3753cbe7c55 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -389,7 +389,7 @@ export class DashboardMigrator { upgradeToGridLayout(old) { let yPos = 0; - let widthFactor = GRID_COLUMN_COUNT / 12; + const widthFactor = GRID_COLUMN_COUNT / 12; const maxPanelId = _.max( _.flattenDeep( @@ -407,15 +407,15 @@ export class DashboardMigrator { // Add special "row" panels if even one row is collapsed, repeated or has visible title const showRows = _.some(old.rows, row => row.collapse || row.showTitle || row.repeat); - for (let row of old.rows) { + for (const row of old.rows) { if (row.repeatIteration) { continue; } - let height: any = row.height || DEFAULT_ROW_HEIGHT; + const height: any = row.height || DEFAULT_ROW_HEIGHT; const rowGridHeight = getGridHeight(height); - let rowPanel: any = {}; + const rowPanel: any = {}; let rowPanelModel: PanelModel; if (showRows) { // add special row panel @@ -436,9 +436,9 @@ export class DashboardMigrator { yPos++; } - let rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); + const rowArea = new RowArea(rowGridHeight, GRID_COLUMN_COUNT, yPos); - for (let panel of row.panels) { + for (const panel of row.panels) { panel.span = panel.span || DEFAULT_PANEL_SPAN; if (panel.minSpan) { panel.minSpan = Math.min(GRID_COLUMN_COUNT, GRID_COLUMN_COUNT / 12 * panel.minSpan); @@ -446,7 +446,7 @@ export class DashboardMigrator { const panelWidth = Math.floor(panel.span) * widthFactor; const panelHeight = panel.height ? getGridHeight(panel.height) : rowGridHeight; - let panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); + const panelPos = rowArea.getPanelPosition(panelHeight, panelWidth); yPos = rowArea.yPos; panel.gridPos = { x: panelPos.x, diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 976e4213920..8f61cf06c60 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -1,7 +1,7 @@ import moment from 'moment'; import _ from 'lodash'; -import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL } from 'app/core/constants'; +import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { DEFAULT_ANNOTATION_COLOR } from 'app/core/utils/colors'; import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; @@ -95,7 +95,7 @@ export class DashboardModel { addBuiltInAnnotationQuery() { let found = false; - for (let item of this.annotations.list) { + for (const item of this.annotations.list) { if (item.builtIn === 1) { found = true; break; @@ -138,7 +138,7 @@ export class DashboardModel { // cleans meta data and other non persistent state getSaveModelClone(options?) { - let defaults = _.defaults(options || {}, { + const defaults = _.defaults(options || {}, { saveVariables: true, saveTimerange: true, }); @@ -160,8 +160,8 @@ export class DashboardModel { if (!defaults.saveVariables) { for (let i = 0; i < copy.templating.list.length; i++) { - let current = copy.templating.list[i]; - let original = _.find(this.originalTemplating, { name: current.name, type: current.type }); + const current = copy.templating.list[i]; + const original = _.find(this.originalTemplating, { name: current.name, type: current.type }); if (!original) { continue; @@ -213,13 +213,13 @@ export class DashboardModel { getNextPanelId() { let max = 0; - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id > max) { max = panel.id; } if (panel.collapsed) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { if (rowPanel.id > max) { max = rowPanel.id; } @@ -237,7 +237,7 @@ export class DashboardModel { } getPanelById(id) { - for (let panel of this.panels) { + for (const panel of this.panels) { if (panel.id === id) { return panel; } @@ -248,7 +248,7 @@ export class DashboardModel { addPanel(panelData) { panelData.id = this.getNextPanelId(); - let panel = new PanelModel(panelData); + const panel = new PanelModel(panelData); this.panels.unshift(panel); @@ -273,15 +273,15 @@ export class DashboardModel { } this.iteration = (this.iteration || new Date().getTime()) + 1; - let panelsToRemove = []; + const panelsToRemove = []; // cleanup scopedVars - for (let panel of this.panels) { + for (const panel of this.panels) { delete panel.scopedVars; } for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if ((!panel.repeat || panel.repeatedByRow) && panel.repeatPanelId && panel.repeatIteration !== this.iteration) { panelsToRemove.push(panel); } @@ -304,7 +304,7 @@ export class DashboardModel { this.iteration = (this.iteration || new Date().getTime()) + 1; for (let i = 0; i < this.panels.length; i++) { - let panel = this.panels[i]; + const panel = this.panels[i]; if (panel.repeat) { this.repeatPanel(panel, i); } @@ -315,9 +315,9 @@ export class DashboardModel { } cleanUpRowRepeats(rowPanels) { - let panelsToRemove = []; + const panelsToRemove = []; for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (!panel.repeat && panel.repeatPanelId) { panelsToRemove.push(panel); } @@ -333,16 +333,16 @@ export class DashboardModel { let rowPanels = row.panels; if (!row.collapsed) { - let rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); + const rowPanelIndex = _.findIndex(this.panels, p => p.id === row.id); rowPanels = this.getRowPanels(rowPanelIndex); } this.cleanUpRowRepeats(rowPanels); for (let i = 0; i < rowPanels.length; i++) { - let panel = rowPanels[i]; + const panel = rowPanels[i]; if (panel.repeat) { - let panelIndex = _.findIndex(this.panels, p => p.id === panel.id); + const panelIndex = _.findIndex(this.panels, p => p.id === panel.id); this.repeatPanel(panel, panelIndex); } } @@ -354,7 +354,7 @@ export class DashboardModel { return sourcePanel; } - let clone = new PanelModel(sourcePanel.getSaveModel()); + const clone = new PanelModel(sourcePanel.getSaveModel()); clone.id = this.getNextPanelId(); // insert after source panel + value index @@ -370,13 +370,13 @@ export class DashboardModel { // if first clone return source if (valueIndex === 0) { if (!sourceRowPanel.collapsed) { - let rowPanels = this.getRowPanels(sourcePanelIndex); + const rowPanels = this.getRowPanels(sourcePanelIndex); sourceRowPanel.panels = rowPanels; } return sourceRowPanel; } - let clone = new PanelModel(sourceRowPanel.getSaveModel()); + const clone = new PanelModel(sourceRowPanel.getSaveModel()); // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels, insertPos; if (sourceRowPanel.collapsed) { @@ -397,7 +397,7 @@ export class DashboardModel { } repeatPanel(panel: PanelModel, panelIndex: number) { - let variable = _.find(this.templating.list, { name: panel.repeat }); + const variable = _.find(this.templating.list, { name: panel.repeat }); if (!variable) { return; } @@ -407,13 +407,13 @@ export class DashboardModel { return; } - let selectedOptions = this.getSelectedVariableOptions(variable); - let minWidth = panel.minSpan || 6; + const selectedOptions = this.getSelectedVariableOptions(variable); + const minWidth = panel.minSpan || 6; let xPos = 0; let yPos = panel.gridPos.y; for (let index = 0; index < selectedOptions.length; index++) { - let option = selectedOptions[index]; + const option = selectedOptions[index]; let copy; copy = this.getPanelRepeatClone(panel, index, panelIndex); @@ -443,9 +443,9 @@ export class DashboardModel { } // Update gridPos for panels below - let yOffset = yPos - panel.gridPos.y; + const yOffset = yPos - panel.gridPos.y; if (yOffset > 0) { - let panelBelowIndex = panelIndex + selectedOptions.length; + const panelBelowIndex = panelIndex + selectedOptions.length; for (let i = panelBelowIndex; i < this.panels.length; i++) { this.panels[i].gridPos.y += yOffset; } @@ -453,7 +453,7 @@ export class DashboardModel { } repeatRow(panel: PanelModel, panelIndex: number, variable) { - let selectedOptions = this.getSelectedVariableOptions(variable); + const selectedOptions = this.getSelectedVariableOptions(variable); let yPos = panel.gridPos.y; function setScopedVars(panel, variableOption) { @@ -462,12 +462,12 @@ export class DashboardModel { } for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) { - let option = selectedOptions[optionIndex]; - let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); + const option = selectedOptions[optionIndex]; + const rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); setScopedVars(rowCopy, option); - let rowHeight = this.getRowHeight(rowCopy); - let rowPanels = rowCopy.panels || []; + const rowHeight = this.getRowHeight(rowCopy); + const rowPanels = rowCopy.panels || []; let panelBelowIndex; if (panel.collapsed) { @@ -483,11 +483,11 @@ export class DashboardModel { panelBelowIndex = panelIndex + optionIndex + 1; } else { // insert after 'row' panel - let insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; + const insertPos = panelIndex + (rowPanels.length + 1) * optionIndex + 1; _.each(rowPanels, (rowPanel, i) => { setScopedVars(rowPanel, option); if (optionIndex > 0) { - let cloneRowPanel = new PanelModel(rowPanel); + const cloneRowPanel = new PanelModel(rowPanel); this.updateRepeatedPanelIds(cloneRowPanel, true); // For exposed row additionally set proper Y grid position and add it to dashboard panels cloneRowPanel.gridPos.y += rowHeight * optionIndex; @@ -650,29 +650,29 @@ export class DashboardModel { formatDate(date, format?) { date = moment.isMoment(date) ? date : moment(date); format = format || 'YYYY-MM-DD HH:mm:ss'; - let timezone = this.getTimezone(); + const timezone = this.getTimezone(); return timezone === 'browser' ? moment(date).format(format) : moment.utc(date).format(format); } destroy() { this.events.removeAllListeners(); - for (let panel of this.panels) { + for (const panel of this.panels) { panel.destroy(); } } toggleRow(row: PanelModel) { - let rowIndex = _.indexOf(this.panels, row); + const rowIndex = _.indexOf(this.panels, row); if (row.collapsed) { row.collapsed = false; - let hasRepeat = _.some(row.panels, p => p.repeat); + const hasRepeat = _.some(row.panels, p => p.repeat); if (row.panels.length > 0) { // Use first panel to figure out if it was moved or pushed - let firstPanel = row.panels[0]; - let yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); + const firstPanel = row.panels[0]; + const yDiff = firstPanel.gridPos.y - (row.gridPos.y + row.gridPos.h); // start inserting after row let insertPos = rowIndex + 1; @@ -680,7 +680,7 @@ export class DashboardModel { // needed to know home much panels below should be pushed down let yMax = row.gridPos.y; - for (let panel of row.panels) { + for (const panel of row.panels) { // make sure y is adjusted (in case row moved while collapsed) // console.log('yDiff', yDiff); panel.gridPos.y -= yDiff; @@ -713,7 +713,7 @@ export class DashboardModel { return; } - let rowPanels = this.getRowPanels(rowIndex); + const rowPanels = this.getRowPanels(rowIndex); // remove panels _.pull(this.panels, ...rowPanels); @@ -729,10 +729,10 @@ export class DashboardModel { * Will return all panels after rowIndex until it encounters another row */ getRowPanels(rowIndex: number): PanelModel[] { - let rowPanels = []; + const rowPanels = []; for (let index = rowIndex + 1; index < this.panels.length; index++) { - let panel = this.panels[index]; + const panel = this.panels[index]; // break when encountering another row if (panel.type === 'row') { @@ -791,7 +791,7 @@ export class DashboardModel { } private updateSchema(old) { - let migrator = new DashboardMigrator(this); + const migrator = new DashboardMigrator(this); migrator.updateSchema(old); } @@ -830,4 +830,32 @@ export class DashboardModel { return !_.isEqual(updated, this.originalTemplating); } + + autoFitPanels(viewHeight: number) { + if (!this.meta.autofitpanels) { + return; + } + + const currentGridHeight = Math.max( + ...this.panels.map(panel => { + return panel.gridPos.h + panel.gridPos.y; + }) + ); + + // Consider navbar and submenu controls, padding and margin + let visibleHeight = window.innerHeight - 55 - 20; + + // Remove submenu if visible + if (this.meta.submenuEnabled) { + visibleHeight -= 50; + } + + const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); + const scaleFactor = currentGridHeight / visibleGridHeight; + + this.panels.forEach((panel, i) => { + panel.gridPos.y = Math.round(panel.gridPos.y / scaleFactor) || 1; + panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; + }); + } } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index f1f2290ce40..a26a0401d56 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -68,18 +68,18 @@ export class AddPanelPanel extends React.Component item) .value(); let copiedPanels = []; - let copiedPanelJson = store.get(LS_PANEL_COPY_KEY); + const copiedPanelJson = store.get(LS_PANEL_COPY_KEY); if (copiedPanelJson) { - let copiedPanel = JSON.parse(copiedPanelJson); - let pluginInfo = _.find(panels, { id: copiedPanel.type }); + const copiedPanel = JSON.parse(copiedPanelJson); + const pluginInfo = _.find(panels, { id: copiedPanel.type }); if (pluginInfo) { - let pluginCopy = _.cloneDeep(pluginInfo); + const pluginCopy = _.cloneDeep(pluginInfo); pluginCopy.name = copiedPanel.title; pluginCopy.sort = -1; pluginCopy.defaults = copiedPanel; @@ -97,7 +97,7 @@ export class AddPanelPanel extends React.Component; } @@ -156,7 +156,7 @@ export class AddPanelPanel extends React.Component { return regex.test(panel.name); }); @@ -189,12 +189,12 @@ export class AddPanelPanel extends React.Component { const layout = []; this.panelMap = {}; - for (let panel of this.dashboard.panels) { - let stringId = panel.id.toString(); + for (const panel of this.dashboard.panels) { + const stringId = panel.id.toString(); this.panelMap[stringId] = panel; if (!panel.gridPos) { @@ -103,7 +103,7 @@ export class DashboardGrid extends React.Component { continue; } - let panelPos: any = { + const panelPos: any = { i: stringId, x: panel.gridPos.x, y: panel.gridPos.y, @@ -174,7 +174,7 @@ export class DashboardGrid extends React.Component { renderPanels() { const panelElements = []; - for (let panel of this.dashboard.panels) { + for (const panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push(
    diff --git a/public/app/features/dashboard/dashgrid/PanelLoader.ts b/public/app/features/dashboard/dashgrid/PanelLoader.ts index beda30bdff7..c654b756085 100644 --- a/public/app/features/dashboard/dashgrid/PanelLoader.ts +++ b/public/app/features/dashboard/dashgrid/PanelLoader.ts @@ -10,8 +10,8 @@ export class PanelLoader { constructor(private $compile, private $rootScope) {} load(elem, panel, dashboard): AttachedPanel { - var template = ''; - var panelScope = this.$rootScope.$new(); + const template = ''; + const panelScope = this.$rootScope.$new(); panelScope.panel = panel; panelScope.dashboard = dashboard; diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 269d4b0bada..6ec272b5ca4 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -3,7 +3,7 @@ diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 628f09349d3..6d6373c7e82 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -13,7 +13,7 @@ export class DashNavCtrl { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope); if (this.dashboard.meta.isSnapshot) { - var meta = this.dashboard.meta; + const meta = this.dashboard.meta; this.titleTooltip = 'Created:  ' + moment(meta.created).calendar(); if (meta.expires) { this.titleTooltip += '
    Expires:  ' + moment(meta.expires).fromNow() + '
    '; @@ -22,7 +22,7 @@ export class DashNavCtrl { } toggleSettings() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else { @@ -32,7 +32,7 @@ export class DashNavCtrl { } close() { - let search = this.$location.search(); + const search = this.$location.search(); if (search.editview) { delete search.editview; } else if (search.fullscreen) { @@ -49,7 +49,7 @@ export class DashNavCtrl { } shareDashboard(tabIndex) { - var modalScope = this.$scope.$new(); + const modalScope = this.$scope.$new(); modalScope.tabIndex = tabIndex; modalScope.dashboard = this.dashboard; diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 2e61ce9f8a8..f99946915d6 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -21,15 +21,15 @@ export class DashExportCtrl { } save() { - var blob = new Blob([angular.toJson(this.dash, true)], { + const blob = new Blob([angular.toJson(this.dash, true)], { type: 'application/json;charset=utf-8', }); saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json'); } saveJson() { - var clone = this.dash; - let editScope = this.$rootScope.$new(); + const clone = this.dash; + const editScope = this.$rootScope.$new(); editScope.object = clone; editScope.enableCopy = true; diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts index fc24de76fcc..d0802be72db 100644 --- a/public/app/features/dashboard/export/exporter.ts +++ b/public/app/features/dashboard/export/exporter.ts @@ -12,23 +12,23 @@ export class DashboardExporter { // this is pretty hacky and needs to be changed dashboard.cleanUpRepeats(); - var saveModel = dashboard.getSaveModelClone(); + const saveModel = dashboard.getSaveModelClone(); saveModel.id = null; // undo repeat cleanup dashboard.processRepeats(); - var inputs = []; - var requires = {}; - var datasources = {}; - var promises = []; - var variableLookup: any = {}; + const inputs = []; + const requires = {}; + const datasources = {}; + const promises = []; + const variableLookup: any = {}; - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { variableLookup[variable.name] = variable; } - var templateizeDatasourceUsage = obj => { + const templateizeDatasourceUsage = obj => { // ignore data source properties that contain a variable if (obj.datasource && obj.datasource.indexOf('$') === 0) { if (variableLookup[obj.datasource.substring(1)]) { @@ -42,7 +42,7 @@ export class DashboardExporter { return; } - var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + const refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); datasources[refName] = { name: refName, label: ds.name, @@ -69,14 +69,14 @@ export class DashboardExporter { } if (panel.targets) { - for (let target of panel.targets) { + for (const target of panel.targets) { if (target.datasource !== undefined) { templateizeDatasourceUsage(target); } } } - var panelDef = config.panels[panel.type]; + const panelDef = config.panels[panel.type]; if (panelDef) { requires['panel' + panelDef.id] = { type: 'panel', @@ -88,19 +88,19 @@ export class DashboardExporter { }; // check up panel data sources - for (let panel of saveModel.panels) { + for (const panel of saveModel.panels) { processPanel(panel); // handle collapsed rows if (panel.collapsed !== undefined && panel.collapsed === true && panel.panels) { - for (let rowPanel of panel.panels) { + for (const rowPanel of panel.panels) { processPanel(rowPanel); } } } // templatize template vars - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'query') { templateizeDatasourceUsage(variable); variable.options = []; @@ -110,7 +110,7 @@ export class DashboardExporter { } // templatize annotations vars - for (let annotationDef of saveModel.annotations.list) { + for (const annotationDef of saveModel.annotations.list) { templateizeDatasourceUsage(annotationDef); } @@ -129,9 +129,9 @@ export class DashboardExporter { }); // templatize constants - for (let variable of saveModel.templating.list) { + for (const variable of saveModel.templating.list) { if (variable.type === 'constant') { - var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); + const refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); inputs.push({ name: refName, type: 'constant', @@ -149,7 +149,7 @@ export class DashboardExporter { } // make inputs and requires a top thing - var newObj = {}; + const newObj = {}; newObj['__inputs'] = inputs; newObj['__requires'] = _.sortBy(requires, ['id']); diff --git a/public/app/features/dashboard/folder_picker/folder_picker.ts b/public/app/features/dashboard/folder_picker/folder_picker.ts index 28338c29d33..352b29d27a0 100644 --- a/public/app/features/dashboard/folder_picker/folder_picker.ts +++ b/public/app/features/dashboard/folder_picker/folder_picker.ts @@ -104,10 +104,7 @@ export class FolderPickerCtrl { appEvents.emit('alert-success', ['Folder Created', 'OK']); this.closeCreateFolder(); - this.folder = { - text: result.title, - value: result.id, - }; + this.folder = { text: result.title, value: result.id }; this.onFolderChange(this.folder); }); } @@ -149,17 +146,14 @@ export class FolderPickerCtrl { folder = result.length > 0 ? result[0] : resetFolder; } } - this.folder = folder; - this.onFolderLoad(); - }); - } - private onFolderLoad() { - if (this.onLoad) { - this.onLoad({ - $folder: { id: this.folder.value, title: this.folder.text }, - }); - } + this.folder = folder; + + // if this is not the same as our initial value notify parent + if (this.folder.id !== this.initialFolderId) { + this.onChange({ $folder: { id: this.folder.value, title: this.folder.text } }); + } + }); } } @@ -176,7 +170,6 @@ export function folderPicker() { labelClass: '@', rootName: '@', onChange: '&', - onLoad: '&', onCreateFolder: '&', enterFolderCreation: '&', exitFolderCreation: '&', diff --git a/public/app/features/dashboard/history/history.ts b/public/app/features/dashboard/history/history.ts index be6ad5af1ba..3563ccc7766 100644 --- a/public/app/features/dashboard/history/history.ts +++ b/public/app/features/dashboard/history/history.ts @@ -67,7 +67,7 @@ export class HistoryListCtrl { } revisionSelectionChanged() { - let selected = _.filter(this.revisions, { checked: true }).length; + const selected = _.filter(this.revisions, { checked: true }).length; this.canCompare = selected === 2; } @@ -134,7 +134,7 @@ export class HistoryListCtrl { .getHistoryList(this.dashboard, options) .then(revisions => { // set formatted dates & default values - for (let rev of revisions) { + for (const rev of revisions) { rev.createdDateString = this.formatDate(rev.created); rev.ageString = this.formatBasicDate(rev.created); rev.checked = false; diff --git a/public/app/features/dashboard/repeat_option/repeat_option.ts b/public/app/features/dashboard/repeat_option/repeat_option.ts index 696c634ddae..01e1d716fc5 100644 --- a/public/app/features/dashboard/repeat_option/repeat_option.ts +++ b/public/app/features/dashboard/repeat_option/repeat_option.ts @@ -1,6 +1,6 @@ import { coreModule } from 'app/core/core'; -var template = ` +const template = `