diff --git a/.circleci/config.yml b/.circleci/config.yml index 44f34d42926..8f2e9b6c1af 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 @@ -89,7 +91,7 @@ jobs: 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 + name: run go vet command: 'go vet ./pkg/...' test-frontend: @@ -156,8 +158,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: @@ -213,9 +272,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: | @@ -237,30 +293,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-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 @@ -270,7 +323,17 @@ workflows: - gometalinter - mysql-integration-test - postgres-integration-test - filters: *filter-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 @@ -309,3 +372,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/.gitignore b/.gitignore index 11df66360d9..2484176a469 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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 5298dcd04f6..4983dbafdcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484) * **Prometheus**: Add $interval, $interval_ms, $range, and $range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) * **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) +* **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) @@ -37,11 +38,18 @@ * **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) * **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) ### 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) + # 5.2.2 (2018-07-25) ### Minor diff --git a/Makefile b/Makefile index c1d755d247d..9e136688eb7 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,12 @@ 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 . + test-go: go test -v ./pkg/... @@ -36,4 +42,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/README.md b/README.md index 322523d703b..b2baf0ece59 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,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. 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/devenv/dev-dashboards/datasource_tests_mssql_unittest.json b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json index 80d3e1a5889..0d291f01a09 100644 --- a/devenv/dev-dashboards/datasource_tests_mssql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mssql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532949769359, + "iteration": 1533713720618, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-mssql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "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, @@ -587,10 +670,10 @@ "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": { @@ -670,10 +753,10 @@ "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": { @@ -753,10 +836,10 @@ "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": { @@ -828,6 +911,89 @@ "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, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1029,7 +1195,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1116,7 +1282,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1201,7 +1367,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1288,7 +1454,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1373,7 +1539,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1460,7 +1626,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1545,7 +1711,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 29, "legend": { @@ -1632,7 +1798,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 30, "legend": { @@ -1719,7 +1885,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 14, "legend": { @@ -1807,7 +1973,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 15, "legend": { @@ -1894,7 +2060,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 25, "legend": { @@ -1982,7 +2148,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 22, "legend": { @@ -2069,7 +2235,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 21, "legend": { @@ -2157,7 +2323,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 26, "legend": { @@ -2244,7 +2410,7 @@ "h": 8, "w": 12, "x": 0, - "y": 89 + "y": 83 }, "id": 23, "legend": { @@ -2332,7 +2498,7 @@ "h": 8, "w": 12, "x": 12, - "y": 89 + "y": 83 }, "id": 24, "legend": { @@ -2542,5 +2708,5 @@ "timezone": "", "title": "Datasource tests - MSSQL (unit test)", "uid": "GlAqcPgmz", - "version": 3 + "version": 10 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json index f684186084a..cec8ebe9d02 100644 --- a/devenv/dev-dashboards/datasource_tests_mysql_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_mysql_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532949531280, + "iteration": 1533714324007, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-mysql-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "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, @@ -587,10 +670,10 @@ "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": { @@ -670,10 +753,10 @@ "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": { @@ -753,10 +836,10 @@ "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": { @@ -828,6 +911,89 @@ "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, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1023,7 +1189,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1110,7 +1276,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1195,7 +1361,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1282,7 +1448,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1367,7 +1533,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1454,7 +1620,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1539,7 +1705,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 14, "legend": { @@ -1627,7 +1793,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 15, "legend": { @@ -1714,7 +1880,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 25, "legend": { @@ -1802,7 +1968,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 22, "legend": { @@ -1889,7 +2055,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 21, "legend": { @@ -1977,7 +2143,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 26, "legend": { @@ -2064,7 +2230,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 23, "legend": { @@ -2152,7 +2318,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 24, "legend": { @@ -2360,5 +2526,5 @@ "timezone": "", "title": "Datasource tests - MySQL (unittest)", "uid": "Hmf8FDkmz", - "version": 1 + "version": 9 } \ No newline at end of file diff --git a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json index 3c2b34df78c..cc93308e116 100644 --- a/devenv/dev-dashboards/datasource_tests_postgres_unittest.json +++ b/devenv/dev-dashboards/datasource_tests_postgres_unittest.json @@ -64,7 +64,7 @@ "editable": true, "gnetId": null, "graphTooltip": 0, - "iteration": 1532951521836, + "iteration": 1533714184500, "links": [], "panels": [ { @@ -338,8 +338,8 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, + "h": 6, + "w": 6, "x": 0, "y": 7 }, @@ -421,9 +421,9 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 8, + "h": 6, + "w": 6, + "x": 6, "y": 7 }, "id": 9, @@ -504,9 +504,9 @@ "datasource": "gdev-postgres-ds-tests", "fill": 2, "gridPos": { - "h": 9, - "w": 8, - "x": 16, + "h": 6, + "w": 6, + "x": 12, "y": 7 }, "id": 10, @@ -579,6 +579,89 @@ "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, @@ -587,10 +670,10 @@ "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": { @@ -670,10 +753,10 @@ "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": { @@ -753,10 +836,10 @@ "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": { @@ -828,6 +911,89 @@ "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, @@ -839,7 +1005,7 @@ "h": 8, "w": 12, "x": 0, - "y": 25 + "y": 19 }, "id": 27, "legend": { @@ -926,7 +1092,7 @@ "h": 8, "w": 12, "x": 12, - "y": 25 + "y": 19 }, "id": 5, "legend": { @@ -1011,7 +1177,7 @@ "h": 8, "w": 12, "x": 0, - "y": 33 + "y": 27 }, "id": 4, "legend": { @@ -1098,7 +1264,7 @@ "h": 8, "w": 12, "x": 12, - "y": 33 + "y": 27 }, "id": 28, "legend": { @@ -1183,7 +1349,7 @@ "h": 8, "w": 12, "x": 0, - "y": 41 + "y": 35 }, "id": 19, "legend": { @@ -1270,7 +1436,7 @@ "h": 8, "w": 12, "x": 12, - "y": 41 + "y": 35 }, "id": 18, "legend": { @@ -1355,7 +1521,7 @@ "h": 8, "w": 12, "x": 0, - "y": 49 + "y": 43 }, "id": 17, "legend": { @@ -1442,7 +1608,7 @@ "h": 8, "w": 12, "x": 12, - "y": 49 + "y": 43 }, "id": 20, "legend": { @@ -1527,7 +1693,7 @@ "h": 8, "w": 12, "x": 0, - "y": 57 + "y": 51 }, "id": 14, "legend": { @@ -1615,7 +1781,7 @@ "h": 8, "w": 12, "x": 12, - "y": 57 + "y": 51 }, "id": 15, "legend": { @@ -1702,7 +1868,7 @@ "h": 8, "w": 12, "x": 0, - "y": 65 + "y": 59 }, "id": 25, "legend": { @@ -1790,7 +1956,7 @@ "h": 8, "w": 12, "x": 12, - "y": 65 + "y": 59 }, "id": 22, "legend": { @@ -1877,7 +2043,7 @@ "h": 8, "w": 12, "x": 0, - "y": 73 + "y": 67 }, "id": 21, "legend": { @@ -1965,7 +2131,7 @@ "h": 8, "w": 12, "x": 12, - "y": 73 + "y": 67 }, "id": 26, "legend": { @@ -2052,7 +2218,7 @@ "h": 8, "w": 12, "x": 0, - "y": 81 + "y": 75 }, "id": 23, "legend": { @@ -2140,7 +2306,7 @@ "h": 8, "w": 12, "x": 12, - "y": 81 + "y": 75 }, "id": 24, "legend": { @@ -2352,6 +2518,5 @@ "timezone": "", "title": "Datasource tests - Postgres (unittest)", "uid": "vHQdlVziz", - "version": 1 -} - + "version": 9 +} \ No newline at end of file diff --git a/docs/sources/features/datasources/elasticsearch.md b/docs/sources/features/datasources/elasticsearch.md index 31ce78f0bfe..d29327cf480 100644 --- a/docs/sources/features/datasources/elasticsearch.md +++ b/docs/sources/features/datasources/elasticsearch.md @@ -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 dabb896ec0f..caaf5a6b321 100644 --- a/docs/sources/features/datasources/mssql.md +++ b/docs/sources/features/datasources/mssql.md @@ -81,7 +81,9 @@ 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* diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index a0e67037005..cdb78deed35 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -64,7 +64,9 @@ 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* diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 35dfcac15c0..2be2db0837b 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -61,7 +61,9 @@ 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, *(extract(epoch from dateColumn)/300)::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* diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2a799b044b3..d81d8a8dcec 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -181,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 @@ -195,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 @@ -697,9 +697,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). diff --git a/package.json b/package.json index c0581c1de43..200285d7a1e 100644 --- a/package.json +++ b/package.json @@ -166,6 +166,7 @@ "mousetrap-global-bind": "^1.1.0", "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", @@ -187,4 +188,4 @@ "resolutions": { "caniuse-db": "1.0.30000772" } -} \ No newline at end of file +} diff --git a/packaging/docker/Dockerfile b/packaging/docker/Dockerfile new file mode 100644 index 00000000000..e2109b74909 --- /dev/null +++ b/packaging/docker/Dockerfile @@ -0,0 +1,51 @@ +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" + +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 +WORKDIR / +ENTRYPOINT [ "/run.sh" ] diff --git a/packaging/docker/README.md b/packaging/docker/README.md new file mode 100644 index 00000000000..d80cd87aebc --- /dev/null +++ b/packaging/docker/README.md @@ -0,0 +1,45 @@ +# Grafana Docker image + +[![CircleCI](https://circleci.com/gh/grafana/grafana-docker.svg?style=svg)](https://circleci.com/gh/grafana/grafana-docker) + +## 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 \ No newline at end of file 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..3cf97d580ca --- /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"; then + echo "pushing ${_docker_repo}:latest" + docker push "${_docker_repo}:latest" +else + 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..2d2318a9210 --- /dev/null +++ b/packaging/docker/run.sh @@ -0,0 +1,82 @@ +#!/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 + grafana-cli --pluginsDir "${GF_PATHS_PLUGINS}" plugins install ${plugin} + 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/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/tsdb/mssql/macros.go b/pkg/tsdb/mssql/macros.go index f33ab1d40be..42e47ce6d3c 100644 --- a/pkg/tsdb/mssql/macros.go +++ b/pkg/tsdb/mssql/macros.go @@ -99,9 +99,13 @@ func (m *msSqlMacroEngine) evaluateMacro(name string, args []string) (string, er 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 { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "previous": + m.query.Model.Set("fillMode", "previous") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/mssql/macros_test.go b/pkg/tsdb/mssql/macros_test.go index ea50c418de7..8362ae05aa6 100644 --- a/pkg/tsdb/mssql/macros_test.go +++ b/pkg/tsdb/mssql/macros_test.go @@ -76,12 +76,25 @@ func TestMacroEngine(t *testing.T) { _, 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()) }) diff --git a/pkg/tsdb/mysql/macros.go b/pkg/tsdb/mysql/macros.go index a56fd1ceb2a..905d424f29a 100644 --- a/pkg/tsdb/mysql/macros.go +++ b/pkg/tsdb/mysql/macros.go @@ -94,9 +94,13 @@ func (m *mySqlMacroEngine) evaluateMacro(name string, args []string) (string, er 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 { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "previous": + m.query.Model.Set("fillMode", "previous") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/mysql/mysql_test.go b/pkg/tsdb/mysql/mysql_test.go index 9947c23498b..ca6df8e360e 100644 --- a/pkg/tsdb/mysql/mysql_test.go +++ b/pkg/tsdb/mysql/mysql_test.go @@ -295,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{ { @@ -320,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() { diff --git a/pkg/tsdb/postgres/macros.go b/pkg/tsdb/postgres/macros.go index 9e337caf3ec..aebdc55d1d7 100644 --- a/pkg/tsdb/postgres/macros.go +++ b/pkg/tsdb/postgres/macros.go @@ -116,9 +116,13 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string, 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 { + switch args[2] { + case "NULL": + m.query.Model.Set("fillMode", "null") + case "previous": + m.query.Model.Set("fillMode", "previous") + default: + m.query.Model.Set("fillMode", "value") floatVal, err := strconv.ParseFloat(args[2], 64) if err != nil { return "", fmt.Errorf("error parsing fill value %v", args[2]) diff --git a/pkg/tsdb/postgres/postgres_test.go b/pkg/tsdb/postgres/postgres_test.go index 3e864dca1e6..9e363529df1 100644 --- a/pkg/tsdb/postgres/postgres_test.go +++ b/pkg/tsdb/postgres/postgres_test.go @@ -276,7 +276,7 @@ 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{ { @@ -303,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 diff --git a/pkg/tsdb/sql_engine.go b/pkg/tsdb/sql_engine.go index 3f681a5cdd7..cbf6d6b4d60 100644 --- a/pkg/tsdb/sql_engine.go +++ b/pkg/tsdb/sql_engine.go @@ -274,9 +274,15 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, fillMissing := query.Model.Get("fill").MustBool(false) var fillInterval float64 fillValue := null.Float{} + fillPrevious := false + if fillMissing { fillInterval = query.Model.Get("fillInterval").MustFloat64() * 1000 - if !query.Model.Get("fillNull").MustBool(false) { + switch query.Model.Get("fillMode").MustString() { + case "null": + case "previous": + fillPrevious = true + case "value": fillValue.Float64 = query.Model.Get("fillValue").MustFloat64() fillValue.Valid = true } @@ -352,6 +358,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, 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 @@ -377,6 +391,14 @@ func (e *sqlQueryEndpoint) transformToTimeSeries(query *Query, rows *core.Rows, 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 { 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/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 178e53198d4..3ee5bceae8b 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -4,6 +4,7 @@ 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'; @@ -16,6 +17,8 @@ import Table from './Table'; import TimePicker, { DEFAULT_RANGE } from './TimePicker'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; +const MAX_HISTORY_ITEMS = 100; + function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; @@ -56,6 +59,7 @@ interface IExploreState { datasourceLoading: boolean | null; datasourceMissing: boolean; graphResult: any; + history: any[]; initialDatasource?: string; latency: number; loading: any; @@ -86,6 +90,7 @@ export class Explore extends React.Component { datasourceMissing: false, graphResult: null, initialDatasource: datasource, + history: [], latency: 0, loading: false, logsResult: null, @@ -138,6 +143,7 @@ export class Explore extends React.Component { const supportsGraph = datasource.meta.metrics; const supportsLogs = datasource.meta.logs; const supportsTable = datasource.meta.metrics; + const datasourceId = datasource.meta.id; let datasourceError = null; try { @@ -147,16 +153,20 @@ export class Explore extends React.Component { datasourceError = (error && error.statusText) || error; } + const historyKey = `grafana.explore.history.${datasourceId}`; + const history = store.getObject(historyKey, []); + this.setState( { datasource, datasourceError, + history, supportsGraph, supportsLogs, supportsTable, datasourceLoading: false, }, - () => datasourceError === null && this.handleSubmit() + () => datasourceError === null && this.onSubmit() ); } @@ -164,7 +174,7 @@ export class Explore extends React.Component { this.el = el; }; - handleAddQueryRow = index => { + onAddQueryRow = index => { const { queries } = this.state; const nextQueries = [ ...queries.slice(0, index + 1), @@ -174,7 +184,7 @@ export class Explore extends React.Component { this.setState({ queries: nextQueries }); }; - handleChangeDatasource = async option => { + onChangeDatasource = async option => { this.setState({ datasource: null, datasourceError: null, @@ -187,61 +197,85 @@ export class Explore extends React.Component { this.setDatasource(datasource); }; - handleChangeQuery = (query, index) => { + onChangeQuery = (value: string, index: number, override?: boolean) => { const { queries } = 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 }); + this.setState({ 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, + queries: ensureQueries(), + tableResult: null, + }); + }; + + onClickCloseSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { onChangeSplit(false); } }; - handleClickGraphButton = () => { + onClickGraphButton = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; - handleClickLogsButton = () => { + onClickLogsButton = () => { this.setState(state => ({ showingLogs: !state.showingLogs })); }; - handleClickSplit = () => { + onClickSplit = () => { const { onChangeSplit } = this.props; if (onChangeSplit) { onChangeSplit(true, this.state); } }; - handleClickTableButton = () => { + onClickTableButton = () => { this.setState(state => ({ showingTable: !state.showingTable })); }; - handleRemoveQueryRow = index => { + onClickTableCell = (columnKey: string, rowValue: string) => { + const { datasource, queries } = this.state; + if (datasource && datasource.modifyQuery) { + const nextQueries = queries.map(q => ({ + ...q, + edited: false, + query: datasource.modifyQuery(q.query, { addFilter: { key: columnKey, value: rowValue } }), + })); + 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 = () => { + onSubmit = () => { const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; if (showingTable && supportsTable) { this.runTableQuery(); @@ -254,6 +288,27 @@ export class Explore extends React.Component { } }; + onQuerySuccess(datasourceId: string, queries: any[]): void { + // save queries to history + let { datasource, history } = 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; instant?: boolean }) { const { datasource, queries, range } = this.state; const resolution = this.el.offsetWidth; @@ -286,6 +341,7 @@ export class Explore extends React.Component { const result = makeTimeSeriesList(res.data, options); const latency = Date.now() - now; this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); + this.onQuerySuccess(datasource.meta.id, queries); } catch (response) { console.error(response); const queryError = response.data ? response.data.error : response; @@ -309,6 +365,7 @@ export class Explore extends React.Component { 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; @@ -332,6 +389,7 @@ export class Explore extends React.Component { 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; @@ -352,6 +410,7 @@ export class Explore extends React.Component { datasourceLoading, datasourceMissing, graphResult, + history, latency, loading, logsResult, @@ -390,18 +449,18 @@ export class Explore extends React.Component { ) : ( -
- -
- )} + + )} {!datasourceMissing ? (