Procházet zdrojové kódy

Merge branch 'master' into alerting_reminder

* master: (84 commits)
  docs: adds info about grafana-dev container
  changelog: add notes about closing #12282
  Added Litre/min and milliLitre/min in Flow (#12282)
  remove papaparse dependency
  list name is deleteDatasources, not delete_datasources
  remove internal influx ifql datasource
  Document the endpoint for deleting an org
  tests: rewrite into table tests
  influxdb: adds mode func to backend
  changelog: add notes about closing #11484
  changelog: add notes about closing #11233
  Remove import
  Fix PR feedback
  Removed papaparse from external plugin exports
  Karma to Jest: query_builder
  dsproxy: move http client variable back
  Karma to Jest: threshold_mapper
  Expose react and slate to external plugins
  Karma to Jest: threshold_manager
  Karma to Jest: query_def, index_pattern
  ...
bergquist před 7 roky
rodič
revize
6cd83e182a
82 změnil soubory, kde provedl 2295 přidání a 1127 odebrání
  1. 10 5
      .circleci/config.yml
  2. 17 0
      CHANGELOG.md
  3. 4 4
      Gopkg.lock
  4. 2 2
      Gopkg.toml
  5. 1 1
      docs/sources/administration/provisioning.md
  6. 1 1
      docs/sources/features/datasources/graphite.md
  7. 1 1
      docs/sources/guides/whats-new-in-v5-1.md
  8. 21 0
      docs/sources/http_api/org.md
  9. 21 13
      docs/sources/installation/configuration.md
  10. 7 0
      docs/sources/installation/docker.md
  11. 0 7
      docs/sources/plugins/developing/datasources.md
  12. 4 4
      docs/sources/project/building_from_source.md
  13. 2 2
      latest.json
  14. 1 0
      pkg/api/dtos/plugins.go
  15. 1 0
      pkg/api/frontendsettings.go
  16. 18 3
      pkg/api/index.go
  17. 20 8
      pkg/api/pluginproxy/ds_proxy.go
  18. 134 0
      pkg/api/pluginproxy/ds_proxy_test.go
  19. 9 0
      pkg/api/pluginproxy/test-data/access-token-1.json
  20. 9 0
      pkg/api/pluginproxy/test-data/access-token-2.json
  21. 1 0
      pkg/api/plugins.go
  22. 3 1
      pkg/plugins/dashboard_importer.go
  23. 1 0
      pkg/plugins/dashboards.go
  24. 10 12
      pkg/services/alerting/notifiers/teams.go
  25. 1 1
      pkg/services/sqlstore/sqlstore.go
  26. 2 2
      pkg/tsdb/elasticsearch/client/client.go
  27. 1 0
      pkg/tsdb/influxdb/query_part.go
  28. 32 78
      pkg/tsdb/influxdb/query_part_test.go
  29. 1 1
      public/app/core/components/grafana_app.ts
  30. 4 0
      public/app/core/components/manage_dashboards/manage_dashboards.html
  31. 10 0
      public/app/core/components/manage_dashboards/manage_dashboards.ts
  32. 2 2
      public/app/core/components/search/search.html
  33. 1 0
      public/app/core/config.ts
  34. 2 1
      public/app/core/services/keybindingSrv.ts
  35. 4 0
      public/app/core/table_model.ts
  36. 10 0
      public/app/core/utils/kbn.ts
  37. 13 13
      public/app/features/alerting/specs/threshold_mapper.jest.ts
  38. 25 1
      public/app/features/dashboard/dashboard_import_ctrl.ts
  39. 12 1
      public/app/features/dashboard/dashgrid/AddPanelPanel.tsx
  40. 11 3
      public/app/features/dashboard/dashgrid/DashboardRow.tsx
  41. 17 14
      public/app/features/dashboard/folder_picker/folder_picker.ts
  42. 16 2
      public/app/features/dashboard/partials/dashboard_import.html
  43. 15 2
      public/app/features/dashboard/save_modal.ts
  44. 194 0
      public/app/features/dashboard/specs/exporter.jest.ts
  45. 0 187
      public/app/features/dashboard/specs/exporter_specs.ts
  46. 1 1
      public/app/features/panel/metrics_panel_ctrl.ts
  47. 1 1
      public/app/features/panel/panel_header.ts
  48. 12 1
      public/app/features/panel/specs/metrics_panel_ctrl.jest.ts
  49. 12 13
      public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts
  50. 4 0
      public/app/features/plugins/ds_edit_ctrl.ts
  51. 2 2
      public/app/features/plugins/partials/ds_http_settings.html
  52. 0 28
      public/app/features/plugins/plugin_component.ts
  53. 17 0
      public/app/features/plugins/plugin_loader.ts
  54. 0 100
      public/app/features/plugins/row_ctrl.ts
  55. 0 5
      public/app/plugins/datasource/elasticsearch/module.ts
  56. 105 106
      public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts
  57. 11 12
      public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts
  58. 57 58
      public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts
  59. 93 0
      public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts
  60. 0 95
      public/app/plugins/datasource/elasticsearch/specs/query_def_specs.ts
  61. 0 1
      public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts
  62. 4 3
      public/app/plugins/panel/dashlist/editor.html
  63. 3 2
      public/app/plugins/panel/dashlist/module.ts
  64. 15 15
      public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts
  65. 20 22
      public/app/plugins/panel/graph/specs/threshold_manager.jest.ts
  66. 11 13
      public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts
  67. 16 0
      public/sass/components/_buttons.scss
  68. 12 4
      public/sass/components/_row.scss
  69. 1 1
      public/sass/pages/_alerting.scss
  70. 15 8
      public/sass/pages/_dashboard.scss
  71. 13 3
      public/sass/pages/_login.scss
  72. 6 8
      public/test/core/utils/version_jest.ts
  73. 18 0
      public/test/jest-setup.ts
  74. 1 1
      scripts/webpack/webpack.hot.js
  75. 531 106
      vendor/github.com/mattn/go-sqlite3/sqlite3-binding.c
  76. 529 104
      vendor/github.com/mattn/go-sqlite3/sqlite3-binding.h
  77. 60 32
      vendor/github.com/mattn/go-sqlite3/sqlite3.go
  78. 2 0
      vendor/github.com/mattn/go-sqlite3/sqlite3_go18.go
  79. 12 0
      vendor/github.com/mattn/go-sqlite3/sqlite3_solaris.go
  80. 12 10
      vendor/github.com/mattn/go-sqlite3/sqlite3_trace.go
  81. 7 0
      vendor/github.com/mattn/go-sqlite3/sqlite3ext.h
  82. 21 0
      vendor/github.com/mattn/go-sqlite3/static_mock.go

+ 10 - 5
.circleci/config.yml

@@ -183,16 +183,21 @@ jobs:
           command: 'sudo pip install awscli'
           command: 'sudo pip install awscli'
       - run:
       - run:
           name: deploy to s3
           name: deploy to s3
-          command: 'aws s3 sync ./dist s3://$BUCKET_NAME/master'
+          command: |
+            # Also
+            cp dist/grafana-latest.linux-x64.tar.gz dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
+            aws s3 sync ./dist s3://$BUCKET_NAME/master
       - run:
       - run:
           name: Trigger Windows build
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
       - run:
       - run:
           name: Trigger Docker build
           name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}'
+          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
       - run:
       - run:
           name: Publish to Grafana.com
           name: Publish to Grafana.com
-          command: './scripts/publish -apiKey ${GRAFANA_COM_API_KEY}'
+          command: |
+            rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
+            ./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
 
 
   deploy-release:
   deploy-release:
     docker:
     docker:
@@ -241,8 +246,8 @@ workflows:
             - mysql-integration-test
             - mysql-integration-test
             - postgres-integration-test
             - postgres-integration-test
           filters:
           filters:
-            branches:
-              only: master
+           branches:
+             only: master
   release:
   release:
     jobs:
     jobs:
       - build-all:
       - build-all:

+ 17 - 0
CHANGELOG.md

@@ -2,6 +2,21 @@
 
 
 ### New Features
 ### New Features
 
 
+* **Dashboard**: Import dashboard to folder [#10796](https://github.com/grafana/grafana/issues/10796)
+
+### Minor
+
+* **Dashboard**: Fix so panel titles doesn't wrap [#11074](https://github.com/grafana/grafana/issues/11074)
+* **Dashboard**: Prevent double-click when saving dashboard [#11963](https://github.com/grafana/grafana/issues/11963)
+* **Dashboard**: AutoFocus the add-panel search filter [#12189](https://github.com/grafana/grafana/pull/12189) thx [@ryantxu](https://github.com/ryantxu)
+* **Units**: W/m2 (energy), l/h (flow) and kPa (pressure) [#11233](https://github.com/grafana/grafana/pull/11233), thx [@flopp999](https://github.com/flopp999)
+* **Units**: Litre/min (flow) and milliLitre/min (flow) [#12282](https://github.com/grafana/grafana/pull/12282), thx [@flopp999](https://github.com/flopp999)
+* **Alerting**: Fix mobile notifications for Microsoft Teams alert notifier [#11484](https://github.com/grafana/grafana/pull/11484), thx [@manacker](https://github.com/manacker)
+
+# 5.2.0-beta1 (2018-06-05)
+
+### New Features
+
 * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
 * **Elasticsearch**: Alerting support [#5893](https://github.com/grafana/grafana/issues/5893), thx [@WPH95](https://github.com/WPH95)
 * **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
 * **Login**: Change admin password after first login [#11882](https://github.com/grafana/grafana/issues/11882)
 * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
 * **Alert list panel**: Updated to support filtering alerts by name, dashboard title, folder, tags [#11500](https://github.com/grafana/grafana/issues/11500), [#8168](https://github.com/grafana/grafana/issues/8168), [#6541](https://github.com/grafana/grafana/issues/6541)
@@ -35,6 +50,8 @@
 * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167)
 * **Alert list panel**: Show alerts for user with viewer role [#11167](https://github.com/grafana/grafana/issues/11167)
 * **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670)
 * **Provisioning**: Verify checksum of dashboards before updating to reduce load on database [#11670](https://github.com/grafana/grafana/issues/11670)
 * **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958)
 * **Provisioning**: Support symlinked files in dashboard provisioning config files [#11958](https://github.com/grafana/grafana/issues/11958)
+* **Dashboard list panel**: Search dashboards by folder [#11525](https://github.com/grafana/grafana/issues/11525)
+* **Sidenav**: Always show server admin link in sidenav if grafana admin [#11657](https://github.com/grafana/grafana/issues/11657)
 
 
 # 5.1.3 (2018-05-16)
 # 5.1.3 (2018-05-16)
 
 

+ 4 - 4
Gopkg.lock

@@ -226,7 +226,7 @@
   version = "v1.1.1"
   version = "v1.1.1"
 
 
 [[projects]]
 [[projects]]
-  branch = "renderer"
+  branch = "master"
   name = "github.com/grafana/grafana-plugin-model"
   name = "github.com/grafana/grafana-plugin-model"
   packages = [
   packages = [
     "go/datasource",
     "go/datasource",
@@ -331,8 +331,8 @@
 [[projects]]
 [[projects]]
   name = "github.com/mattn/go-sqlite3"
   name = "github.com/mattn/go-sqlite3"
   packages = ["."]
   packages = ["."]
-  revision = "6c771bb9887719704b210e87e934f08be014bdb1"
-  version = "v1.6.0"
+  revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
+  version = "v1.7.0"
 
 
 [[projects]]
 [[projects]]
   name = "github.com/matttproud/golang_protobuf_extensions"
   name = "github.com/matttproud/golang_protobuf_extensions"
@@ -670,6 +670,6 @@
 [solve-meta]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-name = "dep"
   analyzer-version = 1
   analyzer-version = 1
-  inputs-digest = "6c7ae4bcbe7fa4430d3bdbf204df1b7c59cba88151fbcefa167ce15e6351b6d3"
+  inputs-digest = "85cc057e0cc074ab5b43bd620772d63d51e07b04e8782fcfe55e6929d2fc40f7"
   solver-name = "gps-cdcl"
   solver-name = "gps-cdcl"
   solver-version = 1
   solver-version = 1

+ 2 - 2
Gopkg.toml

@@ -100,7 +100,7 @@ ignored = [
   version = "1.1.1"
   version = "1.1.1"
 
 
 [[constraint]]
 [[constraint]]
-  branch = "renderer"
+  branch = "master"
   name = "github.com/grafana/grafana-plugin-model"
   name = "github.com/grafana/grafana-plugin-model"
 
 
 [[constraint]]
 [[constraint]]
@@ -129,7 +129,7 @@ ignored = [
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/mattn/go-sqlite3"
   name = "github.com/mattn/go-sqlite3"
-  version = "1.6.0"
+  version = "1.7.0"
 
 
 [[constraint]]
 [[constraint]]
   name = "github.com/opentracing/opentracing-go"
   name = "github.com/opentracing/opentracing-go"

+ 1 - 1
docs/sources/administration/provisioning.md

@@ -76,7 +76,7 @@ Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://gith
 
 
 > This feature is available from v5.0
 > This feature is available from v5.0
 
 
-It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `delete_datasources`. Grafana will delete datasources listed in `delete_datasources` before inserting/updating those in the `datasource` list.
+It's possible to manage datasources in Grafana by adding one or more yaml config files in the [`provisioning/datasources`](/installation/configuration/#provisioning) directory. Each config file can contain a list of `datasources` that will be added or updated during start up. If the datasource already exists, Grafana will update it to match the configuration file. The config file can also contain a list of datasources that should be deleted. That list is called `deleteDatasources`. Grafana will delete datasources listed in `deleteDatasources` before inserting/updating those in the `datasource` list.
 
 
 ### Running Multiple Grafana Instances
 ### Running Multiple Grafana Instances
 
 

+ 1 - 1
docs/sources/features/datasources/graphite.md

@@ -20,7 +20,7 @@ queries through the use of query references.
 ## Adding the data source
 ## Adding the data source
 
 
 1. Open the side menu by clicking the Grafana icon in the top header.
 1. Open the side menu by clicking the Grafana icon in the top header.
-2. In the side menu under the `Dashboards` link you should find a link named `Data Sources`.
+2. In the side menu under the `Configuration` link you should find a link named `Data Sources`.
 3. Click the `+ Add data source` button in the top header.
 3. Click the `+ Add data source` button in the top header.
 4. Select `Graphite` from the *Type* dropdown.
 4. Select `Graphite` from the *Type* dropdown.
 
 

+ 1 - 1
docs/sources/guides/whats-new-in-v5-1.md

@@ -115,7 +115,7 @@ Grafana v5.1 brings an improved workflow for provisioned dashboards:
 
 
 
 
 Available options in the dialog will let you `Copy JSON to Clipboard` and/or `Save JSON to file` which can help you synchronize your dashboard changes back to the provisioning source.
 Available options in the dialog will let you `Copy JSON to Clipboard` and/or `Save JSON to file` which can help you synchronize your dashboard changes back to the provisioning source.
-More information in the [Provisioning documentation](/features/datasources/prometheus/).
+More information in the [Provisioning documentation](/administration/provisioning/).
 
 
 <div class="clearfix"></div>
 <div class="clearfix"></div>
 
 

+ 21 - 0
docs/sources/http_api/org.md

@@ -331,6 +331,27 @@ Content-Type: application/json
 {"message":"Organization updated"}
 {"message":"Organization updated"}
 ```
 ```
 
 
+## Delete Organisation
+
+`DELETE /api/orgs/:orgId`
+
+**Example Request**:
+
+```http
+DELETE /api/orgs/1 HTTP/1.1
+Accept: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+{"message":"Organization deleted"}
+```
+
 ## Get Users in Organisation
 ## Get Users in Organisation
 
 
 `GET /api/orgs/:orgId/users`
 `GET /api/orgs/:orgId/users`

+ 21 - 13
docs/sources/installation/configuration.md

@@ -419,25 +419,33 @@ allowed_organizations = github google
 
 
 ## [auth.google]
 ## [auth.google]
 
 
-You need to create a Google project. You can do this in the [Google
-Developer Console](https://console.developers.google.com/project).  When
-you create the project you will need to specify a callback URL. Specify
-this as callback:
+First, you need to create a Google OAuth Client:
 
 
-```bash
-http://<my_grafana_server_name_or_ip>:<grafana_server_port>/login/google
-```
+1. Go to https://console.developers.google.com/apis/credentials
 
 
-This callback URL must match the full HTTP address that you use in your
-browser to access Grafana, but with the prefix path of `/login/google`.
-When the Google project is created you will get a Client ID and a Client
-Secret. Specify these in the Grafana configuration file. For example:
+2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the
+menu that drops down
+
+3. Enter the following:
+
+   - Application Type: Web Application
+   - Name: Grafana
+   - Authorized Javascript Origins: https://grafana.mycompany.com
+   - Authorized Redirect URLs: https://grafana.mycompany.com/login/google
+
+   Replace https://grafana.mycompany.com with the URL of your Grafana instance.
+
+4. Click Create
+
+5. Copy the Client ID and Client Secret from the 'OAuth Client' modal
+
+Specify the Client ID and Secret in the Grafana configuration file. For example:
 
 
 ```bash
 ```bash
 [auth.google]
 [auth.google]
 enabled = true
 enabled = true
-client_id = YOUR_GOOGLE_APP_CLIENT_ID
-client_secret = YOUR_GOOGLE_APP_CLIENT_SECRET
+client_id = CLIENT_ID
+client_secret = CLIENT_SECRET
 scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
 scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email
 auth_url = https://accounts.google.com/o/oauth2/auth
 auth_url = https://accounts.google.com/o/oauth2/auth
 token_url = https://accounts.google.com/o/oauth2/token
 token_url = https://accounts.google.com/o/oauth2/token

+ 7 - 0
docs/sources/installation/docker.md

@@ -49,6 +49,11 @@ $ docker run \
   grafana/grafana:5.1.0
   grafana/grafana:5.1.0
 ```
 ```
 
 
+## Running of 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. 
+
 ## Installing Plugins for Grafana
 ## Installing Plugins for Grafana
 
 
 Pass the plugins you want installed to docker with the `GF_INSTALL_PLUGINS` environment variable as a comma separated list. This will pass each plugin name to `grafana-cli plugins install ${plugin}` and install them when Grafana starts.
 Pass the plugins you want installed to docker with the `GF_INSTALL_PLUGINS` environment variable as a comma separated list. This will pass each plugin name to `grafana-cli plugins install ${plugin}` and install them when Grafana starts.
@@ -132,6 +137,8 @@ docker run -d --user $ID --volume "$PWD/data:/var/lib/grafana" -p 3000:3000 graf
 
 
 ## Reading secrets from files (support for Docker Secrets)
 ## Reading secrets from files (support for Docker Secrets)
 
 
+> Available in v5.2.0 and later
+
 It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/<name of secret>` of the container.
 It's possible to supply Grafana with configuration through files. This works well with [Docker Secrets](https://docs.docker.com/engine/swarm/secrets/) as the secrets by default gets mapped into `/run/secrets/<name of secret>` of the container.
 
 
 You can do this with any of the configuration options in conf/grafana.ini by setting `GF_<SectionName>_<KeyName>_FILE` to the path of the file holding the secret.
 You can do this with any of the configuration options in conf/grafana.ini by setting `GF_<SectionName>_<KeyName>_FILE` to the path of the file holding the secret.

+ 0 - 7
docs/sources/plugins/developing/datasources.md

@@ -25,7 +25,6 @@ To interact with the rest of grafana the plugins module file can export 5 differ
 - Datasource (Required)
 - Datasource (Required)
 - QueryCtrl (Required)
 - QueryCtrl (Required)
 - ConfigCtrl (Required)
 - ConfigCtrl (Required)
-- QueryOptionsCtrl
 - AnnotationsQueryCtrl
 - AnnotationsQueryCtrl
 
 
 ## Plugin json
 ## Plugin json
@@ -182,12 +181,6 @@ A JavaScript class that will be instantiated and treated as an Angular controlle
 
 
 Requires a static template or templateUrl variable which will be rendered as the view for this controller.
 Requires a static template or templateUrl variable which will be rendered as the view for this controller.
 
 
-## QueryOptionsCtrl
-
-A JavaScript class that will be instantiated and treated as an Angular controller when the user edits metrics in a panel. This controller is responsible for handling panel wide settings for the datasource, such as interval, rate and aggregations if needed.
-
-Requires a static template or templateUrl variable which will be rendered as the view for this controller.
-
 ## AnnotationsQueryCtrl
 ## AnnotationsQueryCtrl
 
 
 A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.
 A JavaScript class that will be instantiated and treated as an Angular controller when the user choose this type of datasource in the templating menu in the dashboard.

+ 4 - 4
docs/sources/project/building_from_source.md

@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
 
 
 ## Dependencies
 ## Dependencies
 
 
-- [Go 1.9.2](https://golang.org/dl/)
+- [Go 1.10](https://golang.org/dl/)
 - [Git](https://git-scm.com/downloads)
 - [Git](https://git-scm.com/downloads)
 - [NodeJS LTS](https://nodejs.org/download/)
 - [NodeJS LTS](https://nodejs.org/download/)
 - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
 - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details.
@@ -66,13 +66,13 @@ You can run a local instance of Grafana by running:
 ```bash
 ```bash
 ./bin/grafana-server
 ./bin/grafana-server
 ```
 ```
-If you built the binary with `go run build.go build`, run `./bin/grafana-server`
+Or, if you built the binary with `go run build.go build`, run `./bin/<os>-<architecture>/grafana-server`
 
 
 If you built it with `go build .`, run `./grafana`
 If you built it with `go build .`, run `./grafana`
 
 
 Open grafana in your browser (default [http://localhost:3000](http://localhost:3000)) and login with admin user (default user/pass = admin/admin).
 Open grafana in your browser (default [http://localhost:3000](http://localhost:3000)) and login with admin user (default user/pass = admin/admin).
 
 
-## Developing Grafana
+# Developing Grafana
 
 
 To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that
 To add features, customize your config, etc, you'll need to rebuild the backend when you change the source code. We use a tool named `bra` that
 does this.
 does this.
@@ -124,7 +124,7 @@ Learn more about Grafana config options in the [Configuration section](/installa
 ## Create a pull requests
 ## Create a pull requests
 Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome.
 Please contribute to the Grafana project and submit a pull request! Build new features, write or update documentation, fix bugs and generally make Grafana even more awesome.
 
 
-## Troubleshooting
+# Troubleshooting
 
 
 **Problem**: PhantomJS or node-sass errors when running grunt
 **Problem**: PhantomJS or node-sass errors when running grunt
 
 

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
 {
-  "stable": "5.0.4",
-  "testing": "5.0.4"
+  "stable": "5.1.3",
+  "testing": "5.1.3"
 }
 }

+ 1 - 0
pkg/api/dtos/plugins.go

@@ -57,4 +57,5 @@ type ImportDashboardCommand struct {
 	Overwrite bool                           `json:"overwrite"`
 	Overwrite bool                           `json:"overwrite"`
 	Dashboard *simplejson.Json               `json:"dashboard"`
 	Dashboard *simplejson.Json               `json:"dashboard"`
 	Inputs    []plugins.ImportDashboardInput `json:"inputs"`
 	Inputs    []plugins.ImportDashboardInput `json:"inputs"`
+	FolderId  int64                          `json:"folderId"`
 }
 }

+ 1 - 0
pkg/api/frontendsettings.go

@@ -140,6 +140,7 @@ func getFrontendSettingsMap(c *m.ReqContext) (map[string]interface{}, error) {
 		"authProxyEnabled":        setting.AuthProxyEnabled,
 		"authProxyEnabled":        setting.AuthProxyEnabled,
 		"ldapEnabled":             setting.LdapEnabled,
 		"ldapEnabled":             setting.LdapEnabled,
 		"alertingEnabled":         setting.AlertingEnabled,
 		"alertingEnabled":         setting.AlertingEnabled,
+		"exploreEnabled":          setting.ExploreEnabled,
 		"googleAnalyticsId":       setting.GoogleAnalyticsId,
 		"googleAnalyticsId":       setting.GoogleAnalyticsId,
 		"disableLoginForm":        setting.DisableLoginForm,
 		"disableLoginForm":        setting.DisableLoginForm,
 		"externalUserMngInfo":     setting.ExternalUserMngInfo,
 		"externalUserMngInfo":     setting.ExternalUserMngInfo,

+ 18 - 3
pkg/api/index.go

@@ -99,9 +99,10 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 
 
 		if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
 		if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
 			children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"})
 			children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"})
-			children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"})
 		}
 		}
 
 
+		children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"})
+
 		data.NavTree = append(data.NavTree, &dtos.NavLink{
 		data.NavTree = append(data.NavTree, &dtos.NavLink{
 			Text:     "Create",
 			Text:     "Create",
 			Id:       "create",
 			Id:       "create",
@@ -233,7 +234,7 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 		}
 		}
 	}
 	}
 
 
-	if c.OrgRole == m.ROLE_ADMIN {
+	if c.IsGrafanaAdmin || c.OrgRole == m.ROLE_ADMIN {
 		cfgNode := &dtos.NavLink{
 		cfgNode := &dtos.NavLink{
 			Id:       "cfg",
 			Id:       "cfg",
 			Text:     "Configuration",
 			Text:     "Configuration",
@@ -287,10 +288,24 @@ func setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
 			},
 			},
 		}
 		}
 
 
-		if c.IsGrafanaAdmin {
+		if c.OrgRole != m.ROLE_ADMIN {
+			cfgNode = &dtos.NavLink{
+				Id:       "cfg",
+				Text:     "Configuration",
+				SubTitle: "Organization: " + c.OrgName,
+				Icon:     "gicon gicon-cog",
+				Url:      setting.AppSubUrl + "/admin/users",
+				Children: make([]*dtos.NavLink, 0),
+			}
+		}
+
+		if c.OrgRole == m.ROLE_ADMIN && c.IsGrafanaAdmin {
 			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
 			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
 				Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
 				Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
 			})
 			})
+		}
+
+		if c.IsGrafanaAdmin {
 			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
 			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
 				Text:         "Server Admin",
 				Text:         "Server Admin",
 				HideFromTabs: true,
 				HideFromTabs: true,

+ 20 - 8
pkg/api/pluginproxy/ds_proxy.go

@@ -25,12 +25,9 @@ import (
 )
 )
 
 
 var (
 var (
-	logger = log.New("data-proxy-log")
-	client = &http.Client{
-		Timeout:   time.Second * 30,
-		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
-	}
-	tokenCache = map[int64]*jwtToken{}
+	logger     = log.New("data-proxy-log")
+	tokenCache = map[string]*jwtToken{}
+	client     = newHTTPClient()
 )
 )
 
 
 type jwtToken struct {
 type jwtToken struct {
@@ -48,6 +45,10 @@ type DataSourceProxy struct {
 	plugin    *plugins.DataSourcePlugin
 	plugin    *plugins.DataSourcePlugin
 }
 }
 
 
+type httpClient interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
 func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
 func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx *m.ReqContext, proxyPath string) *DataSourceProxy {
 	targetURL, _ := url.Parse(ds.Url)
 	targetURL, _ := url.Parse(ds.Url)
 
 
@@ -60,6 +61,13 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
 	}
 	}
 }
 }
 
 
+func newHTTPClient() httpClient {
+	return &http.Client{
+		Timeout:   time.Second * 30,
+		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
+	}
+}
+
 func (proxy *DataSourceProxy) HandleRequest() {
 func (proxy *DataSourceProxy) HandleRequest() {
 	if err := proxy.validateRequest(); err != nil {
 	if err := proxy.validateRequest(); err != nil {
 		proxy.ctx.JsonApiErr(403, err.Error(), nil)
 		proxy.ctx.JsonApiErr(403, err.Error(), nil)
@@ -311,7 +319,7 @@ func (proxy *DataSourceProxy) applyRoute(req *http.Request) {
 }
 }
 
 
 func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
 func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error) {
-	if cachedToken, found := tokenCache[proxy.ds.Id]; found {
+	if cachedToken, found := tokenCache[proxy.getAccessTokenCacheKey()]; found {
 		if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
 		if cachedToken.ExpiresOn.After(time.Now().Add(time.Second * 10)) {
 			logger.Info("Using token from cache")
 			logger.Info("Using token from cache")
 			return cachedToken.AccessToken, nil
 			return cachedToken.AccessToken, nil
@@ -350,12 +358,16 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
 
 
 	expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
 	expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
 	token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
 	token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
-	tokenCache[proxy.ds.Id] = &token
+	tokenCache[proxy.getAccessTokenCacheKey()] = &token
 
 
 	logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
 	logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
 	return token.AccessToken, nil
 	return token.AccessToken, nil
 }
 }
 
 
+func (proxy *DataSourceProxy) getAccessTokenCacheKey() string {
+	return fmt.Sprintf("%v_%v_%v", proxy.ds.Id, proxy.route.Path, proxy.route.Method)
+}
+
 func interpolateString(text string, data templateData) (string, error) {
 func interpolateString(text string, data templateData) (string, error) {
 	t, err := template.New("content").Parse(text)
 	t, err := template.New("content").Parse(text)
 	if err != nil {
 	if err != nil {

+ 134 - 0
pkg/api/pluginproxy/ds_proxy_test.go

@@ -1,9 +1,13 @@
 package pluginproxy
 package pluginproxy
 
 
 import (
 import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 	"testing"
 	"testing"
+	"time"
 
 
 	macaron "gopkg.in/macaron.v1"
 	macaron "gopkg.in/macaron.v1"
 
 
@@ -100,6 +104,112 @@ func TestDSRouteRule(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		Convey("Plugin with multiple routes for token auth", func() {
+			plugin := &plugins.DataSourcePlugin{
+				Routes: []*plugins.AppPluginRoute{
+					{
+						Path: "pathwithtoken1",
+						Url:  "https://api.nr1.io/some/path",
+						TokenAuth: &plugins.JwtTokenAuth{
+							Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
+							Params: map[string]string{
+								"grant_type":    "client_credentials",
+								"client_id":     "{{.JsonData.clientId}}",
+								"client_secret": "{{.SecureJsonData.clientSecret}}",
+								"resource":      "https://api.nr1.io",
+							},
+						},
+					},
+					{
+						Path: "pathwithtoken2",
+						Url:  "https://api.nr2.io/some/path",
+						TokenAuth: &plugins.JwtTokenAuth{
+							Url: "https://login.server.com/{{.JsonData.tenantId}}/oauth2/token",
+							Params: map[string]string{
+								"grant_type":    "client_credentials",
+								"client_id":     "{{.JsonData.clientId}}",
+								"client_secret": "{{.SecureJsonData.clientSecret}}",
+								"resource":      "https://api.nr2.io",
+							},
+						},
+					},
+				},
+			}
+
+			setting.SecretKey = "password"
+			key, _ := util.Encrypt([]byte("123"), "password")
+
+			ds := &m.DataSource{
+				JsonData: simplejson.NewFromAny(map[string]interface{}{
+					"clientId": "asd",
+					"tenantId": "mytenantId",
+				}),
+				SecureJsonData: map[string][]byte{
+					"clientSecret": key,
+				},
+			}
+
+			req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
+			ctx := &m.ReqContext{
+				Context: &macaron.Context{
+					Req: macaron.Request{Request: req},
+				},
+				SignedInUser: &m.SignedInUser{OrgRole: m.ROLE_EDITOR},
+			}
+
+			Convey("When creating and caching access tokens", func() {
+				var authorizationHeaderCall1 string
+				var authorizationHeaderCall2 string
+
+				Convey("first call should add authorization header with access token", func() {
+					json, err := ioutil.ReadFile("./test-data/access-token-1.json")
+					So(err, ShouldBeNil)
+
+					client = newFakeHTTPClient(json)
+					proxy1 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
+					proxy1.route = plugin.Routes[0]
+					proxy1.applyRoute(req)
+
+					authorizationHeaderCall1 = req.Header.Get("Authorization")
+					So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
+					So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
+
+					Convey("second call to another route should add a different access token", func() {
+						json2, err := ioutil.ReadFile("./test-data/access-token-2.json")
+						So(err, ShouldBeNil)
+
+						req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
+						client = newFakeHTTPClient(json2)
+						proxy2 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken2")
+						proxy2.route = plugin.Routes[1]
+						proxy2.applyRoute(req)
+
+						authorizationHeaderCall2 = req.Header.Get("Authorization")
+
+						So(req.URL.String(), ShouldEqual, "https://api.nr2.io/some/path")
+						So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
+						So(authorizationHeaderCall2, ShouldStartWith, "Bearer eyJ0e")
+						So(authorizationHeaderCall2, ShouldNotEqual, authorizationHeaderCall1)
+
+						Convey("third call to first route should add cached access token", func() {
+							req, _ := http.NewRequest("GET", "http://localhost/asd", nil)
+
+							client = newFakeHTTPClient([]byte{})
+							proxy3 := NewDataSourceProxy(ds, plugin, ctx, "pathwithtoken1")
+							proxy3.route = plugin.Routes[0]
+							proxy3.applyRoute(req)
+
+							authorizationHeaderCall3 := req.Header.Get("Authorization")
+							So(req.URL.String(), ShouldEqual, "https://api.nr1.io/some/path")
+							So(authorizationHeaderCall1, ShouldStartWith, "Bearer eyJ0e")
+							So(authorizationHeaderCall3, ShouldStartWith, "Bearer eyJ0e")
+							So(authorizationHeaderCall3, ShouldEqual, authorizationHeaderCall1)
+						})
+					})
+				})
+			})
+		})
+
 		Convey("When proxying graphite", func() {
 		Convey("When proxying graphite", func() {
 			plugin := &plugins.DataSourcePlugin{}
 			plugin := &plugins.DataSourcePlugin{}
 			ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
 			ds := &m.DataSource{Url: "htttp://graphite:8080", Type: m.DS_GRAPHITE}
@@ -214,3 +324,27 @@ func TestDSRouteRule(t *testing.T) {
 
 
 	})
 	})
 }
 }
+
+type httpClientStub struct {
+	fakeBody []byte
+}
+
+func (c *httpClientStub) Do(req *http.Request) (*http.Response, error) {
+	bodyJSON, _ := simplejson.NewJson(c.fakeBody)
+	_, passedTokenCacheTest := bodyJSON.CheckGet("expires_on")
+	So(passedTokenCacheTest, ShouldBeTrue)
+
+	bodyJSON.Set("expires_on", fmt.Sprint(time.Now().Add(time.Second*60).Unix()))
+	body, _ := bodyJSON.MarshalJSON()
+	resp := &http.Response{
+		Body: ioutil.NopCloser(bytes.NewReader(body)),
+	}
+
+	return resp, nil
+}
+
+func newFakeHTTPClient(fakeBody []byte) httpClient {
+	return &httpClientStub{
+		fakeBody: fakeBody,
+	}
+}

+ 9 - 0
pkg/api/pluginproxy/test-data/access-token-1.json

@@ -0,0 +1,9 @@
+{
+  "token_type": "Bearer",
+  "expires_in": "3599",
+  "ext_expires_in": "0",
+  "expires_on": "1528740417",
+  "not_before": "1528736517",
+  "resource": "https://api.nr1.io",
+  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL2FwaS5sb2dhbmFseXRpY3MuaW8iLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9lN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgvIiwiaWF0IjoxNTI4NzM2NTE3LCJuYmYiOjE1Mjg3MzY1MTcsImV4cCI6MTUyODc0MDQxNywiYWlvIjoiWTJkZ1lBaStzaWRsT3NmQ2JicGhLMSsremttN0NBQT0iLCJhcHBpZCI6IjdmMzJkYjdjLTZmNmYtNGU4OC05M2Q5LTlhZTEyNmMwYTU1ZiIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJvaWQiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJzdWIiOiI1NDQ5ZmJjOS1mYWJhLTRkNjItODE2Yy05ZmMwMzZkMWViN2UiLCJ0aWQiOiJlN2YzZjY2MS1hOTMzLTRiM2YtODE3Ni01MWM0Zjk4MmVjNDgiLCJ1dGkiOiJZQTlQa2lxUy1VV1hMQjhIRnU0U0FBIiwidmVyIjoiMS4wIn0.ga5qudt4LDMKTStAxUmzjyZH8UFBAaFirJqpTdmYny4NtkH6JT2EILvjTjYxlKeTQisvwx9gof0PyicZIab9d6wlMa2xiLzr2nmaOonYClY8fqBaRTgc1xVjrKFw5SCgpx3FnEyJhIWvVPIfaWaogSHcQbIpe4kdk4tz-ccmrx0D1jsziSI4BZcJcX04aJuHZGz9k4mQZ_AA5sQSeQaNuojIng6rYoIifAXFYBZPTbeeeqmiGq8v0IOLeNKbC0POeQCJC_KKBG6Z_MV2KgPxFEzQuX2ZFmRD_wGPteV5TUBxh1kARdqexA3e0zAKSawR9kmrAiZ21lPr4tX2Br_HDg"
+}

+ 9 - 0
pkg/api/pluginproxy/test-data/access-token-2.json

@@ -0,0 +1,9 @@
+{
+  "token_type": "Bearer",
+  "expires_in": "3599",
+  "ext_expires_in": "0",
+  "expires_on": "1528662059",
+  "not_before": "1528658159",
+  "resource": "https://api.nr2.io",
+  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayIsImtpZCI6ImlCakwxUmNxemhpeTRmcHhJeGRacW9oTTJZayJ9.eyJhdWQiOiJodHRwczovL21hbmFnZW1lbnQuYXp1cmUuY29tLyIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2U3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OC8iLCJpYXQiOjE1Mjg2NTgxNTksIm5iZiI6MTUyODY1ODE1OSwiZXhwIjoxNTI4NjYyMDU5LCJhaW8iOiJZMmRnWUFpK3NpZGxPc2ZDYmJwaEsxKyt6a203Q0FBPSIsImFwcGlkIjoiODg5YjdlZDgtMWFlZC00ODZlLTk3ODktODE5NzcwYmJiNjFhIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvZTdmM2Y2NjEtYTkzMy00YjNmLTgxNzYtNTFjNGY5ODJlYzQ4LyIsIm9pZCI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInN1YiI6IjY0YzQxNjMyLTliOWUtNDczNy05MTYwLTBlNjAzZTg3NjljYyIsInRpZCI6ImU3ZjNmNjYxLWE5MzMtNGIzZi04MTc2LTUxYzRmOTgyZWM0OCIsInV0aSI6IkQ1ODZHSGUySDBPd0ptOU0xeVlKQUEiLCJ2ZXIiOiIxLjAifQ.Pw8c8gpoZptw3lGreQoHQaMVOozSaTE5D38Vm2aCHRB3DvD3N-Qcm1x0ZCakUEV2sJd7jvx4XtPFuW7063T0V1deExL4rzzvIo0ZfMmURf9tCTiKFKYibqf8_PtfPSz0t9eNDEUGmWDh1Wgssb4W_H-wPqgl9VPMT7T6ynkfIm0-ODPZTBzgSHiY8C_L1-DkhsK7XiqbUlSDgx9FpfChZS3ah8QhA8geqnb_HVuSktg7WhpxmogSpK5QdrwSE3jsbItpzOfLJ4iBd2ExzS2C0y8H_Coluk3Y1YA07tAxJ6Y7oBv-XwGqNfZhveOCQOzX-U3dFod3fXXysjB0UB89WQ"
+}

+ 1 - 0
pkg/api/plugins.go

@@ -174,6 +174,7 @@ func ImportDashboard(c *m.ReqContext, apiCmd dtos.ImportDashboardCommand) Respon
 		Path:      apiCmd.Path,
 		Path:      apiCmd.Path,
 		Inputs:    apiCmd.Inputs,
 		Inputs:    apiCmd.Inputs,
 		Overwrite: apiCmd.Overwrite,
 		Overwrite: apiCmd.Overwrite,
+		FolderId:  apiCmd.FolderId,
 		Dashboard: apiCmd.Dashboard,
 		Dashboard: apiCmd.Dashboard,
 	}
 	}
 
 

+ 3 - 1
pkg/plugins/dashboard_importer.go

@@ -16,6 +16,7 @@ type ImportDashboardCommand struct {
 	Path      string
 	Path      string
 	Inputs    []ImportDashboardInput
 	Inputs    []ImportDashboardInput
 	Overwrite bool
 	Overwrite bool
+	FolderId  int64
 
 
 	OrgId    int64
 	OrgId    int64
 	User     *m.SignedInUser
 	User     *m.SignedInUser
@@ -70,7 +71,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
 		UserId:    cmd.User.UserId,
 		UserId:    cmd.User.UserId,
 		Overwrite: cmd.Overwrite,
 		Overwrite: cmd.Overwrite,
 		PluginId:  cmd.PluginId,
 		PluginId:  cmd.PluginId,
-		FolderId:  dashboard.FolderId,
+		FolderId:  cmd.FolderId,
 	}
 	}
 
 
 	dto := &dashboards.SaveDashboardDTO{
 	dto := &dashboards.SaveDashboardDTO{
@@ -91,6 +92,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error {
 		Title:            savedDash.Title,
 		Title:            savedDash.Title,
 		Path:             cmd.Path,
 		Path:             cmd.Path,
 		Revision:         savedDash.Data.Get("revision").MustInt64(1),
 		Revision:         savedDash.Data.Get("revision").MustInt64(1),
+		FolderId:         savedDash.FolderId,
 		ImportedUri:      "db/" + savedDash.Slug,
 		ImportedUri:      "db/" + savedDash.Slug,
 		ImportedUrl:      savedDash.GetUrl(),
 		ImportedUrl:      savedDash.GetUrl(),
 		ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),
 		ImportedRevision: dashboard.Data.Get("revision").MustInt64(1),

+ 1 - 0
pkg/plugins/dashboards.go

@@ -17,6 +17,7 @@ type PluginDashboardInfoDTO struct {
 	ImportedUrl      string `json:"importedUrl"`
 	ImportedUrl      string `json:"importedUrl"`
 	Slug             string `json:"slug"`
 	Slug             string `json:"slug"`
 	DashboardId      int64  `json:"dashboardId"`
 	DashboardId      int64  `json:"dashboardId"`
+	FolderId         int64  `json:"folderId"`
 	ImportedRevision int64  `json:"importedRevision"`
 	ImportedRevision int64  `json:"importedRevision"`
 	Revision         int64  `json:"revision"`
 	Revision         int64  `json:"revision"`
 	Description      string `json:"description"`
 	Description      string `json:"description"`

+ 10 - 12
pkg/services/alerting/notifiers/teams.go

@@ -41,10 +41,8 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 
 
 type TeamsNotifier struct {
 type TeamsNotifier struct {
 	NotifierBase
 	NotifierBase
-	Url       string
-	Recipient string
-	Mention   string
-	log       log.Logger
+	Url string
+	log log.Logger
 }
 }
 
 
 func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
 func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -75,17 +73,17 @@ func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
 		})
 		})
 	}
 	}
 
 
-	message := this.Mention
-	if evalContext.Rule.State != m.AlertStateOK { //don't add message when going back to alert state ok.
-		message += " " + evalContext.Rule.Message
-	} else {
-		message += " " // summary must not be empty
+	message := ""
+	if evalContext.Rule.State != m.AlertStateOK { //dont add message when going back to alert state ok.
+		message = evalContext.Rule.Message
 	}
 	}
 
 
 	body := map[string]interface{}{
 	body := map[string]interface{}{
-		"@type":      "MessageCard",
-		"@context":   "http://schema.org/extensions",
-		"summary":    message,
+		"@type":    "MessageCard",
+		"@context": "http://schema.org/extensions",
+		// summary MUST not be empty or the webhook request fails
+		// summary SHOULD contain some meaningful information, since it is used for mobile notifications
+		"summary":    evalContext.GetNotificationTitle(),
 		"title":      evalContext.GetNotificationTitle(),
 		"title":      evalContext.GetNotificationTitle(),
 		"themeColor": evalContext.GetStateModel().Color,
 		"themeColor": evalContext.GetStateModel().Color,
 		"sections": []map[string]interface{}{
 		"sections": []map[string]interface{}{

+ 1 - 1
pkg/services/sqlstore/sqlstore.go

@@ -89,7 +89,7 @@ func (ss *SqlStore) ensureAdminUser() error {
 	systemUserCountQuery := m.GetSystemUserCountStatsQuery{}
 	systemUserCountQuery := m.GetSystemUserCountStatsQuery{}
 
 
 	if err := bus.Dispatch(&systemUserCountQuery); err != nil {
 	if err := bus.Dispatch(&systemUserCountQuery); err != nil {
-		fmt.Errorf("Could not determine if admin user exists: %v", err)
+		return fmt.Errorf("Could not determine if admin user exists: %v", err)
 	}
 	}
 
 
 	if systemUserCountQuery.Result.Count > 0 {
 	if systemUserCountQuery.Result.Count > 0 {

+ 2 - 2
pkg/tsdb/elasticsearch/client/client.go

@@ -43,12 +43,12 @@ type Client interface {
 var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb.TimeRange) (Client, error) {
 var NewClient = func(ctx context.Context, ds *models.DataSource, timeRange *tsdb.TimeRange) (Client, error) {
 	version, err := ds.JsonData.Get("esVersion").Int()
 	version, err := ds.JsonData.Get("esVersion").Int()
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("eleasticsearch version is required, err=%v", err)
+		return nil, fmt.Errorf("elasticsearch version is required, err=%v", err)
 	}
 	}
 
 
 	timeField, err := ds.JsonData.Get("timeField").String()
 	timeField, err := ds.JsonData.Get("timeField").String()
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("eleasticsearch time field name is required, err=%v", err)
+		return nil, fmt.Errorf("elasticsearch time field name is required, err=%v", err)
 	}
 	}
 
 
 	indexInterval := ds.JsonData.Get("interval").MustString()
 	indexInterval := ds.JsonData.Get("interval").MustString()

+ 1 - 0
pkg/tsdb/influxdb/query_part.go

@@ -31,6 +31,7 @@ func init() {
 	renders["mean"] = QueryDefinition{Renderer: functionRenderer}
 	renders["mean"] = QueryDefinition{Renderer: functionRenderer}
 	renders["median"] = QueryDefinition{Renderer: functionRenderer}
 	renders["median"] = QueryDefinition{Renderer: functionRenderer}
 	renders["sum"] = QueryDefinition{Renderer: functionRenderer}
 	renders["sum"] = QueryDefinition{Renderer: functionRenderer}
+	renders["mode"] = QueryDefinition{Renderer: functionRenderer}
 
 
 	renders["holt_winters"] = QueryDefinition{
 	renders["holt_winters"] = QueryDefinition{
 		Renderer: functionRenderer,
 		Renderer: functionRenderer,

+ 32 - 78
pkg/tsdb/influxdb/query_part_test.go

@@ -4,85 +4,39 @@ import (
 	"testing"
 	"testing"
 
 
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
-	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
 func TestInfluxdbQueryPart(t *testing.T) {
 func TestInfluxdbQueryPart(t *testing.T) {
-	Convey("Influxdb query parts", t, func() {
-
-		queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
-		query := &Query{}
-
-		Convey("render field ", func() {
-			part, err := NewQueryPart("field", []string{"value"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "value")
-			So(res, ShouldEqual, `"value"`)
-		})
-
-		Convey("render nested part", func() {
-			part, err := NewQueryPart("derivative", []string{"10s"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "mean(value)")
-			So(res, ShouldEqual, "derivative(mean(value), 10s)")
-		})
-
-		Convey("render bottom", func() {
-			part, err := NewQueryPart("bottom", []string{"3"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "value")
-			So(res, ShouldEqual, "bottom(value, 3)")
-		})
-
-		Convey("render time with $interval", func() {
-			part, err := NewQueryPart("time", []string{"$interval"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "")
-			So(res, ShouldEqual, "time($interval)")
-		})
-
-		Convey("render time with auto", func() {
-			part, err := NewQueryPart("time", []string{"auto"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "")
-			So(res, ShouldEqual, "time($__interval)")
-		})
-
-		Convey("render spread", func() {
-			part, err := NewQueryPart("spread", []string{})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "value")
-			So(res, ShouldEqual, `spread(value)`)
-		})
-
-		Convey("render suffix", func() {
-			part, err := NewQueryPart("math", []string{"/ 100"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "mean(value)")
-			So(res, ShouldEqual, "mean(value) / 100")
-		})
-
-		Convey("render alias", func() {
-			part, err := NewQueryPart("alias", []string{"test"})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "mean(value)")
-			So(res, ShouldEqual, `mean(value) AS "test"`)
-		})
-
-		Convey("render count distinct", func() {
-			part, err := NewQueryPart("count", []string{})
-			So(err, ShouldBeNil)
-
-			res := part.Render(query, queryContext, "distinct(value)")
-			So(res, ShouldEqual, `count(distinct(value))`)
-		})
-	})
+	tcs := []struct {
+		mode     string
+		input    string
+		params   []string
+		expected string
+	}{
+		{mode: "field", params: []string{"value"}, input: "value", expected: `"value"`},
+		{mode: "derivative", params: []string{"10s"}, input: "mean(value)", expected: `derivative(mean(value), 10s)`},
+		{mode: "bottom", params: []string{"3"}, input: "value", expected: `bottom(value, 3)`},
+		{mode: "time", params: []string{"$interval"}, input: "", expected: `time($interval)`},
+		{mode: "time", params: []string{"auto"}, input: "", expected: `time($__interval)`},
+		{mode: "spread", params: []string{}, input: "value", expected: `spread(value)`},
+		{mode: "math", params: []string{"/ 100"}, input: "mean(value)", expected: `mean(value) / 100`},
+		{mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`},
+		{mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`},
+		{mode: "mode", params: []string{}, input: "value", expected: `mode(value)`},
+	}
+
+	queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")}
+	query := &Query{}
+
+	for _, tc := range tcs {
+		part, err := NewQueryPart(tc.mode, tc.params)
+		if err != nil {
+			t.Errorf("Expected NewQueryPart to not return an error. error: %v", err)
+		}
+
+		res := part.Render(query, queryContext, tc.input)
+		if res != tc.expected {
+			t.Errorf("expected %v to render into %s", tc, tc.expected)
+		}
+	}
 }
 }

+ 1 - 1
public/app/core/components/grafana_app.ts

@@ -199,7 +199,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
       body.mousemove(userActivityDetected);
       body.mousemove(userActivityDetected);
       body.keydown(userActivityDetected);
       body.keydown(userActivityDetected);
       // set useCapture = true to catch event here
       // set useCapture = true to catch event here
-      document.addEventListener('wheel', userActivityDetected, true);
+      document.addEventListener('wheel', userActivityDetected, { capture: true, passive: true });
       // treat tab change as activity
       // treat tab change as activity
       document.addEventListener('visibilitychange', userActivityDetected);
       document.addEventListener('visibilitychange', userActivityDetected);
 
 

+ 4 - 0
public/app/core/components/manage_dashboards/manage_dashboards.html

@@ -13,6 +13,10 @@
       <i class="fa fa-plus"></i>
       <i class="fa fa-plus"></i>
       Folder
       Folder
     </a>
     </a>
+    <a class="btn btn-success" href="{{ctrl.importDashboardUrl()}}" ng-if="ctrl.hasEditPermissionInFolders || ctrl.canSave">
+      <i class="fa fa-plus"></i>
+      Import
+    </a>
   </div>
   </div>
 
 
   <div class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters">
   <div class="page-action-bar page-action-bar--narrow" ng-show="ctrl.hasFilters">

+ 10 - 0
public/app/core/components/manage_dashboards/manage_dashboards.ts

@@ -294,6 +294,16 @@ export class ManageDashboardsCtrl {
 
 
     return url;
     return url;
   }
   }
+
+  importDashboardUrl() {
+    let url = 'dashboard/import';
+
+    if (this.folderId) {
+      url += `?folderId=${this.folderId}`;
+    }
+
+    return url;
+  }
 }
 }
 
 
 export function manageDashboardsDirective() {
 export function manageDashboardsDirective() {

+ 2 - 2
public/app/core/components/search/search.html

@@ -52,11 +52,11 @@
         <a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor">
         <a href="dashboards/folder/new" class="search-filter-box-link" ng-if="ctrl.isEditor">
           <i class="gicon gicon-folder-new"></i> New folder
           <i class="gicon gicon-folder-new"></i> New folder
         </a>
         </a>
-        <a href="dashboard/import" class="search-filter-box-link" ng-if="ctrl.isEditor">
+        <a href="dashboard/import" class="search-filter-box-link" ng-if="ctrl.isEditor || ctrl.hasEditPermissionInFolders">
           <i class="gicon gicon-dashboard-import"></i> Import dashboard
           <i class="gicon gicon-dashboard-import"></i> Import dashboard
         </a>
         </a>
         <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
         <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
-          <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find  dashboards on Grafana.com
+          <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find dashboards on Grafana.com
         </a>
         </a>
       </div>
       </div>
     </div>
     </div>

+ 1 - 0
public/app/core/config.ts

@@ -16,6 +16,7 @@ class Settings {
   defaultDatasource: string;
   defaultDatasource: string;
   alertingEnabled: boolean;
   alertingEnabled: boolean;
   authProxyEnabled: boolean;
   authProxyEnabled: boolean;
+  exploreEnabled: boolean;
   ldapEnabled: boolean;
   ldapEnabled: boolean;
   oauth: any;
   oauth: any;
   disableUserSignUp: boolean;
   disableUserSignUp: boolean;

+ 2 - 1
public/app/core/services/keybindingSrv.ts

@@ -1,6 +1,7 @@
 import $ from 'jquery';
 import $ from 'jquery';
 import _ from 'lodash';
 import _ from 'lodash';
 
 
+import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
 import { encodePathComponent } from 'app/core/utils/location_util';
 import { encodePathComponent } from 'app/core/utils/location_util';
@@ -178,7 +179,7 @@ export class KeybindingSrv {
     });
     });
 
 
     // jump to explore if permissions allow
     // jump to explore if permissions allow
-    if (this.contextSrv.isEditor) {
+    if (this.contextSrv.isEditor && config.exploreEnabled) {
       this.bind('x', async () => {
       this.bind('x', async () => {
         if (dashboard.meta.focusPanelId) {
         if (dashboard.meta.focusPanelId) {
           const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
           const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);

+ 4 - 0
public/app/core/table_model.ts

@@ -44,4 +44,8 @@ export default class TableModel {
       this.columnMap[col.text] = col;
       this.columnMap[col.text] = col;
     }
     }
   }
   }
+
+  addRow(row) {
+    this.rows.push(row);
+  }
 }
 }

+ 10 - 0
public/app/core/utils/kbn.ts

@@ -499,6 +499,7 @@ kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
 kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
 kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
 kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1);
 kbn.valueFormats.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1);
 kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
 kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
+kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2');
 kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
 kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
 kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
 kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
 kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
 kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
@@ -528,6 +529,7 @@ kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar');
 kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1);
 kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1);
 kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
 kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
 kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
 kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
+kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa');
 kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
 kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
 kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']);
 kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']);
 
 
@@ -579,6 +581,9 @@ kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm');
 kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
 kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
 kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
 kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
 kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
 kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
+kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h');
+kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('L');
+kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('L', -1);
 
 
 // Angle
 // Angle
 kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
 kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
@@ -1014,6 +1019,7 @@ kbn.getUnitFormats = function() {
         { text: 'Watt (W)', value: 'watt' },
         { text: 'Watt (W)', value: 'watt' },
         { text: 'Kilowatt (kW)', value: 'kwatt' },
         { text: 'Kilowatt (kW)', value: 'kwatt' },
         { text: 'Milliwatt (mW)', value: 'mwatt' },
         { text: 'Milliwatt (mW)', value: 'mwatt' },
+        { text: 'Watt per square metre (W/m2)', value: 'Wm2' },
         { text: 'Volt-ampere (VA)', value: 'voltamp' },
         { text: 'Volt-ampere (VA)', value: 'voltamp' },
         { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
         { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
         { text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
         { text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
@@ -1049,6 +1055,7 @@ kbn.getUnitFormats = function() {
         { text: 'Bars', value: 'pressurebar' },
         { text: 'Bars', value: 'pressurebar' },
         { text: 'Kilobars', value: 'pressurekbar' },
         { text: 'Kilobars', value: 'pressurekbar' },
         { text: 'Hectopascals', value: 'pressurehpa' },
         { text: 'Hectopascals', value: 'pressurehpa' },
+        { text: 'Kilopascals', value: 'pressurekpa' },
         { text: 'Inches of mercury', value: 'pressurehg' },
         { text: 'Inches of mercury', value: 'pressurehg' },
         { text: 'PSI', value: 'pressurepsi' },
         { text: 'PSI', value: 'pressurepsi' },
       ],
       ],
@@ -1069,6 +1076,9 @@ kbn.getUnitFormats = function() {
         { text: 'Cubic meters/sec (cms)', value: 'flowcms' },
         { text: 'Cubic meters/sec (cms)', value: 'flowcms' },
         { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
         { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
         { text: 'Cubic feet/min (cfm)', value: 'flowcfm' },
         { text: 'Cubic feet/min (cfm)', value: 'flowcfm' },
+        { text: 'Litre/hour', value: 'litreh' },
+        { text: 'Litre/min (l/min)', value: 'flowlpm' },
+        { text: 'milliLitre/min (mL/min)', value: 'flowmlpm' },
       ],
       ],
     },
     },
     {
     {

+ 13 - 13
public/app/features/alerting/specs/threshold_mapper_specs.ts → public/app/features/alerting/specs/threshold_mapper.jest.ts

@@ -18,9 +18,9 @@ describe('ThresholdMapper', () => {
       };
       };
 
 
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
-      expect(updated).to.be(true);
-      expect(panel.thresholds[0].op).to.be('gt');
-      expect(panel.thresholds[0].value).to.be(100);
+      expect(updated).toBe(true);
+      expect(panel.thresholds[0].op).toBe('gt');
+      expect(panel.thresholds[0].value).toBe(100);
     });
     });
   });
   });
 
 
@@ -39,12 +39,12 @@ describe('ThresholdMapper', () => {
       };
       };
 
 
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
-      expect(updated).to.be(true);
-      expect(panel.thresholds[0].op).to.be('lt');
-      expect(panel.thresholds[0].value).to.be(100);
+      expect(updated).toBe(true);
+      expect(panel.thresholds[0].op).toBe('lt');
+      expect(panel.thresholds[0].value).toBe(100);
 
 
-      expect(panel.thresholds[1].op).to.be('gt');
-      expect(panel.thresholds[1].value).to.be(200);
+      expect(panel.thresholds[1].op).toBe('gt');
+      expect(panel.thresholds[1].value).toBe(200);
     });
     });
   });
   });
 
 
@@ -63,12 +63,12 @@ describe('ThresholdMapper', () => {
       };
       };
 
 
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
       var updated = ThresholdMapper.alertToGraphThresholds(panel);
-      expect(updated).to.be(true);
-      expect(panel.thresholds[0].op).to.be('gt');
-      expect(panel.thresholds[0].value).to.be(100);
+      expect(updated).toBe(true);
+      expect(panel.thresholds[0].op).toBe('gt');
+      expect(panel.thresholds[0].value).toBe(100);
 
 
-      expect(panel.thresholds[1].op).to.be('lt');
-      expect(panel.thresholds[1].value).to.be(200);
+      expect(panel.thresholds[1].op).toBe('lt');
+      expect(panel.thresholds[1].value).toBe(200);
     });
     });
   });
   });
 });
 });

+ 25 - 1
public/app/features/dashboard/dashboard_import_ctrl.ts

@@ -21,6 +21,9 @@ export class DashboardImportCtrl {
   uidValidationError: any;
   uidValidationError: any;
   autoGenerateUid: boolean;
   autoGenerateUid: boolean;
   autoGenerateUidValue: string;
   autoGenerateUidValue: string;
+  folderId: number;
+  initialFolderTitle: string;
+  isValidFolderSelection: boolean;
 
 
   /** @ngInject */
   /** @ngInject */
   constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
   constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
@@ -31,6 +34,8 @@ export class DashboardImportCtrl {
     this.uidExists = false;
     this.uidExists = false;
     this.autoGenerateUid = true;
     this.autoGenerateUid = true;
     this.autoGenerateUidValue = 'auto-generated';
     this.autoGenerateUidValue = 'auto-generated';
+    this.folderId = $routeParams.folderId ? Number($routeParams.folderId) || 0 : null;
+    this.initialFolderTitle = 'Select a folder';
 
 
     // check gnetId in url
     // check gnetId in url
     if ($routeParams.gnetId) {
     if ($routeParams.gnetId) {
@@ -102,8 +107,9 @@ export class DashboardImportCtrl {
     this.nameExists = false;
     this.nameExists = false;
 
 
     this.validationSrv
     this.validationSrv
-      .validateNewDashboardName(0, this.dash.title)
+      .validateNewDashboardName(this.folderId, this.dash.title)
       .then(() => {
       .then(() => {
+        this.nameExists = false;
         this.hasNameValidationError = false;
         this.hasNameValidationError = false;
       })
       })
       .catch(err => {
       .catch(err => {
@@ -138,6 +144,23 @@ export class DashboardImportCtrl {
       });
       });
   }
   }
 
 
+  onFolderChange(folder) {
+    this.folderId = folder.id;
+    this.titleChanged();
+  }
+
+  onEnterFolderCreation() {
+    this.inputsValid = false;
+  }
+
+  onExitFolderCreation() {
+    this.inputValueChanged();
+  }
+
+  isValid() {
+    return this.inputsValid && this.folderId !== null;
+  }
+
   saveDashboard() {
   saveDashboard() {
     var inputs = this.inputs.map(input => {
     var inputs = this.inputs.map(input => {
       return {
       return {
@@ -153,6 +176,7 @@ export class DashboardImportCtrl {
         dashboard: this.dash,
         dashboard: this.dash,
         overwrite: true,
         overwrite: true,
         inputs: inputs,
         inputs: inputs,
+        folderId: this.folderId,
       })
       })
       .then(res => {
       .then(res => {
         this.$location.url(res.importedUrl);
         this.$location.url(res.importedUrl);

+ 12 - 1
public/app/features/dashboard/dashgrid/AddPanelPanel.tsx

@@ -154,6 +154,15 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
     });
     });
   }
   }
 
 
+  filterKeyPress(evt) {
+    if (evt.key === 'Enter') {
+      let panel = _.head(this.state.panelPlugins);
+      if (panel) {
+        this.onAddPanel(panel);
+      }
+    }
+  }
+
   filterPanels(panels, filter) {
   filterPanels(panels, filter) {
     let regex = new RegExp(filter, 'i');
     let regex = new RegExp(filter, 'i');
     return panels.filter(panel => {
     return panels.filter(panel => {
@@ -229,10 +238,12 @@ export class AddPanelPanel extends React.Component<AddPanelPanelProps, AddPanelP
               <label className="gf-form gf-form--grow gf-form--has-input-icon">
               <label className="gf-form gf-form--grow gf-form--has-input-icon">
                 <input
                 <input
                   type="text"
                   type="text"
-                  className="gf-form-input max-width-20"
+                  autoFocus
+                  className="gf-form-input gf-form--grow"
                   placeholder="Panel Search Filter"
                   placeholder="Panel Search Filter"
                   value={this.state.filter}
                   value={this.state.filter}
                   onChange={this.filterChange.bind(this)}
                   onChange={this.filterChange.bind(this)}
+                  onKeyPress={this.filterKeyPress.bind(this)}
                 />
                 />
                 <i className="gf-form-input-icon fa fa-search" />
                 <i className="gf-form-input-icon fa fa-search" />
               </label>
               </label>

+ 11 - 3
public/app/features/dashboard/dashgrid/DashboardRow.tsx

@@ -84,15 +84,18 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
       'fa-chevron-right': this.state.collapsed,
       'fa-chevron-right': this.state.collapsed,
     });
     });
 
 
-    let title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
-    const hiddenPanels = this.props.panel.panels ? this.props.panel.panels.length : 0;
+    const title = templateSrv.replaceWithText(this.props.panel.title, this.props.panel.scopedVars);
+    const count = this.props.panel.panels ? this.props.panel.panels.length : 0;
+    const panels = count === 1 ? 'panel' : 'panels';
 
 
     return (
     return (
       <div className={classes}>
       <div className={classes}>
         <a className="dashboard-row__title pointer" onClick={this.toggle}>
         <a className="dashboard-row__title pointer" onClick={this.toggle}>
           <i className={chevronClass} />
           <i className={chevronClass} />
           {title}
           {title}
-          <span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
+          <span className="dashboard-row__panel_count">
+            ({count} {panels})
+          </span>
         </a>
         </a>
         {this.dashboard.meta.canEdit === true && (
         {this.dashboard.meta.canEdit === true && (
           <div className="dashboard-row__actions">
           <div className="dashboard-row__actions">
@@ -104,6 +107,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
             </a>
             </a>
           </div>
           </div>
         )}
         )}
+        {this.state.collapsed === true && (
+          <div className="dashboard-row__toggle-target" onClick={this.toggle}>
+            &nbsp;
+          </div>
+        )}
         <div className="dashboard-row__drag grid-drag-handle" />
         <div className="dashboard-row__drag grid-drag-handle" />
       </div>
       </div>
     );
     );

+ 17 - 14
public/app/features/dashboard/folder_picker/folder_picker.ts

@@ -132,23 +132,26 @@ export class FolderPickerCtrl {
   }
   }
 
 
   private loadInitialValue() {
   private loadInitialValue() {
-    if (this.initialFolderId && this.initialFolderId > 0) {
-      this.getOptions('').then(result => {
-        this.folder = _.find(result, { value: this.initialFolderId });
-        if (!this.folder) {
-          this.folder = { text: this.initialTitle, value: this.initialFolderId };
-        }
-        this.onFolderLoad();
-      });
-    } else {
-      if (this.initialTitle && this.initialFolderId === null) {
-        this.folder = { text: this.initialTitle, value: null };
-      } else {
-        this.folder = { text: this.rootName, value: 0 };
+    const resetFolder = { text: this.initialTitle, value: null };
+    const rootFolder = { text: this.rootName, value: 0 };
+    this.getOptions('').then(result => {
+      let folder;
+      if (this.initialFolderId) {
+        folder = _.find(result, { value: this.initialFolderId });
+      } else if (this.enableReset && this.initialTitle && this.initialFolderId === null) {
+        folder = resetFolder;
       }
       }
 
 
+      if (!folder) {
+        if (this.isEditor) {
+          folder = rootFolder;
+        } else {
+          folder = result.length > 0 ? result[0] : resetFolder;
+        }
+      }
+      this.folder = folder;
       this.onFolderLoad();
       this.onFolderLoad();
-    }
+    });
   }
   }
 
 
   private onFolderLoad() {
   private onFolderLoad() {

+ 16 - 2
public/app/features/dashboard/partials/dashboard_import.html

@@ -80,6 +80,20 @@
         </div>
         </div>
       </div>
       </div>
 
 
+      <div class="gf-form-inline">
+        <div class="gf-form gf-form--grow">
+          <folder-picker  label-class="width-15"
+                          initial-folder-id="ctrl.folderId"
+                          initial-title="ctrl.initialFolderTitle"
+                          on-change="ctrl.onFolderChange($folder)"
+                          on-load="ctrl.onFolderChange($folder)"
+                          enter-folder-creation="ctrl.onEnterFolderCreation()"
+                          exit-folder-creation="ctrl.onExitFolderCreation()"
+                          enable-create-new="true">
+          </folder-picker>
+        </div>
+      </div>
+
       <div class="gf-form-inline">
       <div class="gf-form-inline">
         <div class="gf-form gf-form--grow">
         <div class="gf-form gf-form--grow">
           <span class="gf-form-label width-15">
           <span class="gf-form-label width-15">
@@ -132,10 +146,10 @@
     </div>
     </div>
 
 
     <div class="gf-form-button-row">
     <div class="gf-form-button-row">
-      <button type="button" class="btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.inputsValid">
+      <button type="button" class="btn btn-success width-12" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.isValid()">
         <i class="fa fa-save"></i> Import
         <i class="fa fa-save"></i> Import
       </button>
       </button>
-      <button type="button" class="btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.inputsValid">
+      <button type="button" class="btn btn-danger width-12" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists || ctrl.uidExists" ng-disabled="!ctrl.isValid()">
         <i class="fa fa-save"></i> Import (Overwrite)
         <i class="fa fa-save"></i> Import (Overwrite)
       </button>
       </button>
       <a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>
       <a class="btn btn-link" ng-click="ctrl.back()">Cancel</a>

+ 15 - 2
public/app/features/dashboard/save_modal.ts

@@ -50,8 +50,17 @@ const template = `
     </div>
     </div>
 
 
     <div class="gf-form-button-row text-center">
     <div class="gf-form-button-row text-center">
-      <button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Save</button>
-      <a class="btn btn-link" ng-click="ctrl.dismiss();">Cancel</a>
+      <button
+        id="saveBtn"
+        type="submit"
+        class="btn btn-success"
+        ng-class="{'btn-success--processing': ctrl.isSaving}"
+        ng-disabled="ctrl.saveForm.$invalid || ctrl.isSaving"
+      >
+        <span ng-if="!ctrl.isSaving">Save</span>
+        <span ng-if="ctrl.isSaving === true">Saving...</span>
+      </button>
+      <button class="btn btn-inverse" ng-click="ctrl.dismiss();">Cancel</button>
     </div>
     </div>
   </form>
   </form>
 </div>
 </div>
@@ -68,6 +77,7 @@ export class SaveDashboardModalCtrl {
   originalCurrent = [];
   originalCurrent = [];
   max: number;
   max: number;
   saveForm: any;
   saveForm: any;
+  isSaving: boolean;
   dismiss: () => void;
   dismiss: () => void;
   timeChange = false;
   timeChange = false;
   variableValueChange = false;
   variableValueChange = false;
@@ -76,6 +86,7 @@ export class SaveDashboardModalCtrl {
   constructor(private dashboardSrv) {
   constructor(private dashboardSrv) {
     this.message = '';
     this.message = '';
     this.max = 64;
     this.max = 64;
+    this.isSaving = false;
     this.templating = dashboardSrv.dash.templating.list;
     this.templating = dashboardSrv.dash.templating.list;
 
 
     this.compareTemplating();
     this.compareTemplating();
@@ -126,6 +137,8 @@ export class SaveDashboardModalCtrl {
     var dashboard = this.dashboardSrv.getCurrent();
     var dashboard = this.dashboardSrv.getCurrent();
     var saveModel = dashboard.getSaveModelClone(options);
     var saveModel = dashboard.getSaveModelClone(options);
 
 
+    this.isSaving = true;
+
     return this.dashboardSrv.save(saveModel, options).then(this.dismiss);
     return this.dashboardSrv.save(saveModel, options).then(this.dismiss);
   }
   }
 }
 }

+ 194 - 0
public/app/features/dashboard/specs/exporter.jest.ts

@@ -0,0 +1,194 @@
+jest.mock('app/core/store', () => {
+  return {
+    getBool: jest.fn(),
+  };
+});
+
+import _ from 'lodash';
+import config from 'app/core/config';
+import { DashboardExporter } from '../export/exporter';
+import { DashboardModel } from '../dashboard_model';
+
+describe('given dashboard with repeated panels', () => {
+  var dash, exported;
+
+  beforeEach(done => {
+    dash = {
+      templating: {
+        list: [
+          {
+            name: 'apps',
+            type: 'query',
+            datasource: 'gfdb',
+            current: { value: 'Asd', text: 'Asd' },
+            options: [{ value: 'Asd', text: 'Asd' }],
+          },
+          {
+            name: 'prefix',
+            type: 'constant',
+            current: { value: 'collectd', text: 'collectd' },
+            options: [],
+          },
+          {
+            name: 'ds',
+            type: 'datasource',
+            query: 'testdb',
+            current: { value: 'prod', text: 'prod' },
+            options: [],
+          },
+        ],
+      },
+      annotations: {
+        list: [
+          {
+            name: 'logs',
+            datasource: 'gfdb',
+          },
+        ],
+      },
+      panels: [
+        { id: 6, datasource: 'gfdb', type: 'graph' },
+        { id: 7 },
+        {
+          id: 8,
+          datasource: '-- Mixed --',
+          targets: [{ datasource: 'other' }],
+        },
+        { id: 9, datasource: '$ds' },
+        {
+          id: 2,
+          repeat: 'apps',
+          datasource: 'gfdb',
+          type: 'graph',
+        },
+        { id: 3, repeat: null, repeatPanelId: 2 },
+      ],
+    };
+
+    config.buildInfo = {
+      version: '3.0.2',
+    };
+
+    //Stubs test function calls
+    var datasourceSrvStub = { get: jest.fn(arg => getStub(arg)) };
+
+    config.panels['graph'] = {
+      id: 'graph',
+      name: 'Graph',
+      info: { version: '1.1.0' },
+    };
+
+    dash = new DashboardModel(dash, {});
+    var exporter = new DashboardExporter(datasourceSrvStub);
+    exporter.makeExportable(dash).then(clean => {
+      exported = clean;
+      done();
+    });
+  });
+
+  it('should replace datasource refs', () => {
+    var panel = exported.panels[0];
+    expect(panel.datasource).toBe('${DS_GFDB}');
+  });
+
+  it('should replace datasource in variable query', () => {
+    expect(exported.templating.list[0].datasource).toBe('${DS_GFDB}');
+    expect(exported.templating.list[0].options.length).toBe(0);
+    expect(exported.templating.list[0].current.value).toBe(undefined);
+    expect(exported.templating.list[0].current.text).toBe(undefined);
+  });
+
+  it('should replace datasource in annotation query', () => {
+    expect(exported.annotations.list[1].datasource).toBe('${DS_GFDB}');
+  });
+
+  it('should add datasource as input', () => {
+    expect(exported.__inputs[0].name).toBe('DS_GFDB');
+    expect(exported.__inputs[0].pluginId).toBe('testdb');
+    expect(exported.__inputs[0].type).toBe('datasource');
+  });
+
+  it('should add datasource to required', () => {
+    var require = _.find(exported.__requires, { name: 'TestDB' });
+    expect(require.name).toBe('TestDB');
+    expect(require.id).toBe('testdb');
+    expect(require.type).toBe('datasource');
+    expect(require.version).toBe('1.2.1');
+  });
+
+  it('should not add built in datasources to required', () => {
+    var require = _.find(exported.__requires, { name: 'Mixed' });
+    expect(require).toBe(undefined);
+  });
+
+  it('should add datasources used in mixed mode', () => {
+    var require = _.find(exported.__requires, { name: 'OtherDB' });
+    expect(require).not.toBe(undefined);
+  });
+
+  it('should add panel to required', () => {
+    var require = _.find(exported.__requires, { name: 'Graph' });
+    expect(require.name).toBe('Graph');
+    expect(require.id).toBe('graph');
+    expect(require.version).toBe('1.1.0');
+  });
+
+  it('should add grafana version', () => {
+    var require = _.find(exported.__requires, { name: 'Grafana' });
+    expect(require.type).toBe('grafana');
+    expect(require.id).toBe('grafana');
+    expect(require.version).toBe('3.0.2');
+  });
+
+  it('should add constant template variables as inputs', () => {
+    var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' });
+    expect(input.type).toBe('constant');
+    expect(input.label).toBe('prefix');
+    expect(input.value).toBe('collectd');
+  });
+
+  it('should templatize constant variables', () => {
+    var variable = _.find(exported.templating.list, { name: 'prefix' });
+    expect(variable.query).toBe('${VAR_PREFIX}');
+    expect(variable.current.text).toBe('${VAR_PREFIX}');
+    expect(variable.current.value).toBe('${VAR_PREFIX}');
+    expect(variable.options[0].text).toBe('${VAR_PREFIX}');
+    expect(variable.options[0].value).toBe('${VAR_PREFIX}');
+  });
+});
+
+// Stub responses
+var stubs = [];
+stubs['gfdb'] = {
+  name: 'gfdb',
+  meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
+};
+
+stubs['other'] = {
+  name: 'other',
+  meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
+};
+
+stubs['-- Mixed --'] = {
+  name: 'mixed',
+  meta: {
+    id: 'mixed',
+    info: { version: '1.2.1' },
+    name: 'Mixed',
+    builtIn: true,
+  },
+};
+
+stubs['-- Grafana --'] = {
+  name: '-- Grafana --',
+  meta: {
+    id: 'grafana',
+    info: { version: '1.2.1' },
+    name: 'grafana',
+    builtIn: true,
+  },
+};
+
+function getStub(arg) {
+  return Promise.resolve(stubs[arg]);
+}

+ 0 - 187
public/app/features/dashboard/specs/exporter_specs.ts

@@ -1,187 +0,0 @@
-import { describe, beforeEach, it, sinon, expect } from 'test/lib/common';
-
-import _ from 'lodash';
-import config from 'app/core/config';
-import { DashboardExporter } from '../export/exporter';
-import { DashboardModel } from '../dashboard_model';
-
-describe('given dashboard with repeated panels', function() {
-  var dash, exported;
-
-  beforeEach(done => {
-    dash = {
-      templating: { list: [] },
-      annotations: { list: [] },
-    };
-
-    config.buildInfo = {
-      version: '3.0.2',
-    };
-
-    dash.templating.list.push({
-      name: 'apps',
-      type: 'query',
-      datasource: 'gfdb',
-      current: { value: 'Asd', text: 'Asd' },
-      options: [{ value: 'Asd', text: 'Asd' }],
-    });
-
-    dash.templating.list.push({
-      name: 'prefix',
-      type: 'constant',
-      current: { value: 'collectd', text: 'collectd' },
-      options: [],
-    });
-
-    dash.templating.list.push({
-      name: 'ds',
-      type: 'datasource',
-      query: 'testdb',
-      current: { value: 'prod', text: 'prod' },
-      options: [],
-    });
-
-    dash.annotations.list.push({
-      name: 'logs',
-      datasource: 'gfdb',
-    });
-
-    dash.panels = [
-      { id: 6, datasource: 'gfdb', type: 'graph' },
-      { id: 7 },
-      {
-        id: 8,
-        datasource: '-- Mixed --',
-        targets: [{ datasource: 'other' }],
-      },
-      { id: 9, datasource: '$ds' },
-    ];
-
-    dash.panels.push({
-      id: 2,
-      repeat: 'apps',
-      datasource: 'gfdb',
-      type: 'graph',
-    });
-    dash.panels.push({ id: 3, repeat: null, repeatPanelId: 2 });
-
-    var datasourceSrvStub = { get: sinon.stub() };
-    datasourceSrvStub.get.withArgs('gfdb').returns(
-      Promise.resolve({
-        name: 'gfdb',
-        meta: { id: 'testdb', info: { version: '1.2.1' }, name: 'TestDB' },
-      })
-    );
-    datasourceSrvStub.get.withArgs('other').returns(
-      Promise.resolve({
-        name: 'other',
-        meta: { id: 'other', info: { version: '1.2.1' }, name: 'OtherDB' },
-      })
-    );
-    datasourceSrvStub.get.withArgs('-- Mixed --').returns(
-      Promise.resolve({
-        name: 'mixed',
-        meta: {
-          id: 'mixed',
-          info: { version: '1.2.1' },
-          name: 'Mixed',
-          builtIn: true,
-        },
-      })
-    );
-    datasourceSrvStub.get.withArgs('-- Grafana --').returns(
-      Promise.resolve({
-        name: '-- Grafana --',
-        meta: {
-          id: 'grafana',
-          info: { version: '1.2.1' },
-          name: 'grafana',
-          builtIn: true,
-        },
-      })
-    );
-
-    config.panels['graph'] = {
-      id: 'graph',
-      name: 'Graph',
-      info: { version: '1.1.0' },
-    };
-
-    dash = new DashboardModel(dash, {});
-    var exporter = new DashboardExporter(datasourceSrvStub);
-    exporter.makeExportable(dash).then(clean => {
-      exported = clean;
-      done();
-    });
-  });
-
-  it('should replace datasource refs', function() {
-    var panel = exported.panels[0];
-    expect(panel.datasource).to.be('${DS_GFDB}');
-  });
-
-  it('should replace datasource in variable query', function() {
-    expect(exported.templating.list[0].datasource).to.be('${DS_GFDB}');
-    expect(exported.templating.list[0].options.length).to.be(0);
-    expect(exported.templating.list[0].current.value).to.be(undefined);
-    expect(exported.templating.list[0].current.text).to.be(undefined);
-  });
-
-  it('should replace datasource in annotation query', function() {
-    expect(exported.annotations.list[1].datasource).to.be('${DS_GFDB}');
-  });
-
-  it('should add datasource as input', function() {
-    expect(exported.__inputs[0].name).to.be('DS_GFDB');
-    expect(exported.__inputs[0].pluginId).to.be('testdb');
-    expect(exported.__inputs[0].type).to.be('datasource');
-  });
-
-  it('should add datasource to required', function() {
-    var require = _.find(exported.__requires, { name: 'TestDB' });
-    expect(require.name).to.be('TestDB');
-    expect(require.id).to.be('testdb');
-    expect(require.type).to.be('datasource');
-    expect(require.version).to.be('1.2.1');
-  });
-
-  it('should not add built in datasources to required', function() {
-    var require = _.find(exported.__requires, { name: 'Mixed' });
-    expect(require).to.be(undefined);
-  });
-
-  it('should add datasources used in mixed mode', function() {
-    var require = _.find(exported.__requires, { name: 'OtherDB' });
-    expect(require).to.not.be(undefined);
-  });
-
-  it('should add panel to required', function() {
-    var require = _.find(exported.__requires, { name: 'Graph' });
-    expect(require.name).to.be('Graph');
-    expect(require.id).to.be('graph');
-    expect(require.version).to.be('1.1.0');
-  });
-
-  it('should add grafana version', function() {
-    var require = _.find(exported.__requires, { name: 'Grafana' });
-    expect(require.type).to.be('grafana');
-    expect(require.id).to.be('grafana');
-    expect(require.version).to.be('3.0.2');
-  });
-
-  it('should add constant template variables as inputs', function() {
-    var input = _.find(exported.__inputs, { name: 'VAR_PREFIX' });
-    expect(input.type).to.be('constant');
-    expect(input.label).to.be('prefix');
-    expect(input.value).to.be('collectd');
-  });
-
-  it('should templatize constant variables', function() {
-    var variable = _.find(exported.templating.list, { name: 'prefix' });
-    expect(variable.query).to.be('${VAR_PREFIX}');
-    expect(variable.current.text).to.be('${VAR_PREFIX}');
-    expect(variable.current.value).to.be('${VAR_PREFIX}');
-    expect(variable.options[0].text).to.be('${VAR_PREFIX}');
-    expect(variable.options[0].value).to.be('${VAR_PREFIX}');
-  });
-});

+ 1 - 1
public/app/features/panel/metrics_panel_ctrl.ts

@@ -314,7 +314,7 @@ class MetricsPanelCtrl extends PanelCtrl {
 
 
   getAdditionalMenuItems() {
   getAdditionalMenuItems() {
     const items = [];
     const items = [];
-    if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
+    if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
       items.push({
       items.push({
         text: 'Explore',
         text: 'Explore',
         click: 'ctrl.explore();',
         click: 'ctrl.explore();',

+ 1 - 1
public/app/features/panel/panel_header.ts

@@ -25,7 +25,7 @@ var template = `
       <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
       <li><a ng-click="ctrl.addDataQuery(datasource);"><i class="fa fa-trash"></i> Remove</a></li>
     </ul>
     </ul>
   </span>
   </span>
-  <span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
+  <span class="panel-time-info" ng-if="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>
 </span>`;
 </span>`;
 
 
 function renderMenuItem(item, ctrl) {
 function renderMenuItem(item, ctrl) {

+ 12 - 1
public/app/features/panel/specs/metrics_panel_ctrl.jest.ts

@@ -1,8 +1,19 @@
 jest.mock('app/core/core', () => ({}));
 jest.mock('app/core/core', () => ({}));
+jest.mock('app/core/config', () => {
+  return {
+    exploreEnabled: true,
+    panels: {
+      test: {
+        id: 'test',
+        name: 'test',
+      },
+    },
+  };
+});
 
 
-import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
 import q from 'q';
 import q from 'q';
 import { PanelModel } from 'app/features/dashboard/panel_model';
 import { PanelModel } from 'app/features/dashboard/panel_model';
+import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
 
 
 describe('MetricsPanelCtrl', () => {
 describe('MetricsPanelCtrl', () => {
   let ctrl;
   let ctrl;

+ 12 - 13
public/app/features/playlist/specs/playlist_edit_ctrl_specs.ts → public/app/features/playlist/specs/playlist_edit_ctrl.jest.ts

@@ -1,5 +1,4 @@
 import '../playlist_edit_ctrl';
 import '../playlist_edit_ctrl';
-import { describe, beforeEach, it, expect } from 'test/lib/common';
 import { PlaylistEditCtrl } from '../playlist_edit_ctrl';
 import { PlaylistEditCtrl } from '../playlist_edit_ctrl';
 
 
 describe('PlaylistEditCtrl', () => {
 describe('PlaylistEditCtrl', () => {
@@ -20,13 +19,13 @@ describe('PlaylistEditCtrl', () => {
 
 
   describe('searchresult returns 2 dashboards, ', () => {
   describe('searchresult returns 2 dashboards, ', () => {
     it('found dashboard should be 2', () => {
     it('found dashboard should be 2', () => {
-      expect(ctx.dashboardresult.length).to.be(2);
+      expect(ctx.dashboardresult.length).toBe(2);
     });
     });
 
 
     it('filtred result should be 2', () => {
     it('filtred result should be 2', () => {
       ctx.filterFoundPlaylistItems();
       ctx.filterFoundPlaylistItems();
-      expect(ctx.filteredDashboards.length).to.be(2);
-      expect(ctx.filteredTags.length).to.be(2);
+      expect(ctx.filteredDashboards.length).toBe(2);
+      expect(ctx.filteredTags.length).toBe(2);
     });
     });
 
 
     describe('adds one dashboard to playlist, ', () => {
     describe('adds one dashboard to playlist, ', () => {
@@ -37,16 +36,16 @@ describe('PlaylistEditCtrl', () => {
       });
       });
 
 
       it('playlistitems should be increased by one', () => {
       it('playlistitems should be increased by one', () => {
-        expect(ctx.playlistItems.length).to.be(2);
+        expect(ctx.playlistItems.length).toBe(2);
       });
       });
 
 
       it('filtred playlistitems should be reduced by one', () => {
       it('filtred playlistitems should be reduced by one', () => {
-        expect(ctx.filteredDashboards.length).to.be(1);
-        expect(ctx.filteredTags.length).to.be(1);
+        expect(ctx.filteredDashboards.length).toBe(1);
+        expect(ctx.filteredTags.length).toBe(1);
       });
       });
 
 
       it('found dashboard should be 2', () => {
       it('found dashboard should be 2', () => {
-        expect(ctx.dashboardresult.length).to.be(2);
+        expect(ctx.dashboardresult.length).toBe(2);
       });
       });
 
 
       describe('removes one dashboard from playlist, ', () => {
       describe('removes one dashboard from playlist, ', () => {
@@ -57,14 +56,14 @@ describe('PlaylistEditCtrl', () => {
         });
         });
 
 
         it('playlistitems should be increased by one', () => {
         it('playlistitems should be increased by one', () => {
-          expect(ctx.playlistItems.length).to.be(0);
+          expect(ctx.playlistItems.length).toBe(0);
         });
         });
 
 
         it('found dashboard should be 2', () => {
         it('found dashboard should be 2', () => {
-          expect(ctx.dashboardresult.length).to.be(2);
-          expect(ctx.filteredDashboards.length).to.be(2);
-          expect(ctx.filteredTags.length).to.be(2);
-          expect(ctx.tagresult.length).to.be(2);
+          expect(ctx.dashboardresult.length).toBe(2);
+          expect(ctx.filteredDashboards.length).toBe(2);
+          expect(ctx.filteredTags.length).toBe(2);
+          expect(ctx.tagresult.length).toBe(2);
         });
         });
       });
       });
     });
     });

+ 4 - 0
public/app/features/plugins/ds_edit_ctrl.ts

@@ -211,6 +211,10 @@ coreModule.directive('datasourceHttpSettings', function() {
       pre: function($scope, elem, attrs) {
       pre: function($scope, elem, attrs) {
         // do not show access option if direct access is disabled
         // do not show access option if direct access is disabled
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
+        $scope.showAccessHelp = false;
+        $scope.toggleAccessHelp = function() {
+          $scope.showAccessHelp = !$scope.showAccessHelp;
+        };
 
 
         $scope.getSuggestUrls = function() {
         $scope.getSuggestUrls = function() {
           return [$scope.suggestUrl];
           return [$scope.suggestUrl];

+ 2 - 2
public/app/features/plugins/partials/ds_http_settings.html

@@ -30,7 +30,7 @@
         </div>
         </div>
       </div>
       </div>
       <div class="gf-form">
       <div class="gf-form">
-        <label class="gf-form-label query-keyword pointer" ng-click="ctrl.showAccessHelp = !ctrl.showAccessHelp">
+        <label class="gf-form-label query-keyword pointer" ng-click="toggleAccessHelp()">
           Help&nbsp;
           Help&nbsp;
           <i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
           <i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
           <i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
           <i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
@@ -38,7 +38,7 @@
       </div>
       </div>
     </div>
     </div>
 
 
-    <div class="alert alert-info" ng-show="ctrl.showAccessHelp">
+    <div class="alert alert-info" ng-show="showAccessHelp">
       <div class="alert-body">
       <div class="alert-body">
         <p>
         <p>
           Access mode controls how requests to the data source will be handled.
           Access mode controls how requests to the data source will be handled.

+ 0 - 28
public/app/features/plugins/plugin_component.ts

@@ -6,7 +6,6 @@ import coreModule from 'app/core/core_module';
 import { importPluginModule } from './plugin_loader';
 import { importPluginModule } from './plugin_loader';
 
 
 import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
 import { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
-import { DashboardRowCtrl } from './row_ctrl';
 
 
 /** @ngInject **/
 /** @ngInject **/
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
@@ -59,15 +58,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
   }
   }
 
 
   function loadPanelComponentInfo(scope, attrs) {
   function loadPanelComponentInfo(scope, attrs) {
-    if (scope.panel.type === 'row') {
-      return $q.when({
-        name: 'dashboard-row',
-        bindings: { dashboard: '=', panel: '=' },
-        attrs: { dashboard: 'ctrl.dashboard', panel: 'panel' },
-        Component: DashboardRowCtrl,
-      });
-    }
-
     var componentInfo: any = {
     var componentInfo: any = {
       name: 'panel-plugin-' + scope.panel.type,
       name: 'panel-plugin-' + scope.panel.type,
       bindings: { dashboard: '=', panel: '=', row: '=' },
       bindings: { dashboard: '=', panel: '=', row: '=' },
@@ -136,24 +126,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
           });
           });
         });
         });
       }
       }
-      // QueryOptionsCtrl
-      case 'query-options-ctrl': {
-        return datasourceSrv.get(scope.ctrl.panel.datasource).then(ds => {
-          return importPluginModule(ds.meta.module).then((dsModule): any => {
-            if (!dsModule.QueryOptionsCtrl) {
-              return { notFound: true };
-            }
-
-            return {
-              baseUrl: ds.meta.baseUrl,
-              name: 'query-options-ctrl-' + ds.meta.id,
-              bindings: { panelCtrl: '=' },
-              attrs: { 'panel-ctrl': 'ctrl.panelCtrl' },
-              Component: dsModule.QueryOptionsCtrl,
-            };
-          });
-        });
-      }
       // Annotations
       // Annotations
       case 'annotations-query-ctrl': {
       case 'annotations-query-ctrl': {
         return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
         return importPluginModule(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {

+ 17 - 0
public/app/features/plugins/plugin_loader.ts

@@ -5,6 +5,15 @@ import kbn from 'app/core/utils/kbn';
 import moment from 'moment';
 import moment from 'moment';
 import angular from 'angular';
 import angular from 'angular';
 import jquery from 'jquery';
 import jquery from 'jquery';
+
+// Experimental module exports
+import prismjs from 'prismjs';
+import slate from 'slate';
+import slateReact from 'slate-react';
+import slatePlain from 'slate-plain-serializer';
+import react from 'react';
+import reactDom from 'react-dom';
+
 import config from 'app/core/config';
 import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
 import TableModel from 'app/core/table_model';
@@ -69,6 +78,14 @@ exposeToPlugin('d3', d3);
 exposeToPlugin('rxjs/Subject', Subject);
 exposeToPlugin('rxjs/Subject', Subject);
 exposeToPlugin('rxjs/Observable', Observable);
 exposeToPlugin('rxjs/Observable', Observable);
 
 
+// Experimental modules
+exposeToPlugin('prismjs', prismjs);
+exposeToPlugin('slate', slate);
+exposeToPlugin('slate-react', slateReact);
+exposeToPlugin('slate-plain-serializer', slatePlain);
+exposeToPlugin('react', react);
+exposeToPlugin('react-dom', reactDom);
+
 // backward compatible path
 // backward compatible path
 exposeToPlugin('vendor/npm/rxjs/Rx', {
 exposeToPlugin('vendor/npm/rxjs/Rx', {
   Subject: Subject,
   Subject: Subject,

+ 0 - 100
public/app/features/plugins/row_ctrl.ts

@@ -1,100 +0,0 @@
-import _ from 'lodash';
-
-export class DashboardRowCtrl {
-  static template = `
-    <div class="dashboard-row__center">
-      <div class="dashboard-row__actions-left">
-        <i class="fa fa-chevron-down" ng-hide="ctrl.panel.collapse"></i>
-        <i class="fa fa-chevron-right" ng-show="ctrl.panel.collapse"></i>
-      </div>
-      <a class="dashboard-row__title pointer" ng-click="ctrl.toggle()">
-        <span class="dashboard-row__title-text">
-          {{ctrl.panel.title | interpolateTemplateVars:this}}
-        </span>
-      </a>
-      <div class="dashboard-row__actions-right">
-        <a class="pointer" ng-click="ctrl.openSettings()"><span class="fa fa-cog"></i></a>
-      </div>
-    </div>
-
-  <div class="dashboard-row__panel_count">
-    ({{ctrl.panel.hiddenPanels.length}} hidden panels)
-  </div>
-  <div class="dashboard-row__drag grid-drag-handle">
-  </div>
-  `;
-
-  dashboard: any;
-  panel: any;
-
-  constructor() {
-    this.panel.hiddenPanels = this.panel.hiddenPanels || [];
-  }
-
-  toggle() {
-    if (this.panel.collapse) {
-      let panelIndex = _.indexOf(this.dashboard.panels, this.panel);
-
-      for (let child of this.panel.hiddenPanels) {
-        this.dashboard.panels.splice(panelIndex + 1, 0, child);
-        child.y = this.panel.y + 1;
-        console.log('restoring child', child);
-      }
-
-      this.panel.hiddenPanels = [];
-      this.panel.collapse = false;
-      return;
-    }
-
-    this.panel.collapse = true;
-    let foundRow = false;
-
-    for (let i = 0; i < this.dashboard.panels.length; i++) {
-      let panel = this.dashboard.panels[i];
-
-      if (panel === this.panel) {
-        console.log('found row');
-        foundRow = true;
-        continue;
-      }
-
-      if (!foundRow) {
-        continue;
-      }
-
-      if (panel.type === 'row') {
-        break;
-      }
-
-      this.panel.hiddenPanels.push(panel);
-      console.log('hiding child', panel.id);
-    }
-
-    for (let hiddenPanel of this.panel.hiddenPanels) {
-      this.dashboard.removePanel(hiddenPanel, false);
-    }
-  }
-
-  moveUp() {
-    // let panelIndex = _.indexOf(this.dashboard.panels, this.panel);
-    // let rowAbove = null;
-    // for (let index = panelIndex-1; index > 0; index--) {
-    //   panel = this.dashboard.panels[index];
-    //   if (panel.type === 'row') {
-    //     rowAbove = panel;
-    //   }
-    // }
-    //
-    // if (rowAbove) {
-    //   this.panel.y = rowAbove.y;
-    // }
-  }
-
-  link(scope, elem) {
-    elem.addClass('dashboard-row');
-
-    scope.$watch('ctrl.panel.collapse', () => {
-      elem.toggleClass('dashboard-row--collapse', this.panel.collapse === true);
-    });
-  }
-}

+ 0 - 5
public/app/plugins/datasource/elasticsearch/module.ts

@@ -2,10 +2,6 @@ import { ElasticDatasource } from './datasource';
 import { ElasticQueryCtrl } from './query_ctrl';
 import { ElasticQueryCtrl } from './query_ctrl';
 import { ElasticConfigCtrl } from './config_ctrl';
 import { ElasticConfigCtrl } from './config_ctrl';
 
 
-class ElasticQueryOptionsCtrl {
-  static templateUrl = 'partials/query.options.html';
-}
-
 class ElasticAnnotationsQueryCtrl {
 class ElasticAnnotationsQueryCtrl {
   static templateUrl = 'partials/annotations.editor.html';
   static templateUrl = 'partials/annotations.editor.html';
 }
 }
@@ -14,6 +10,5 @@ export {
   ElasticDatasource as Datasource,
   ElasticDatasource as Datasource,
   ElasticQueryCtrl as QueryCtrl,
   ElasticQueryCtrl as QueryCtrl,
   ElasticConfigCtrl as ConfigCtrl,
   ElasticConfigCtrl as ConfigCtrl,
-  ElasticQueryOptionsCtrl as QueryOptionsCtrl,
   ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
   ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl,
 };
 };

+ 105 - 106
public/app/plugins/datasource/elasticsearch/specs/elastic_response_specs.ts → public/app/plugins/datasource/elasticsearch/specs/elastic_response.jest.ts

@@ -1,13 +1,12 @@
-import { describe, beforeEach, it, expect } from 'test/lib/common';
 import { ElasticResponse } from '../elastic_response';
 import { ElasticResponse } from '../elastic_response';
 
 
-describe('ElasticResponse', function() {
+describe('ElasticResponse', () => {
   var targets;
   var targets;
   var response;
   var response;
   var result;
   var result;
 
 
-  describe('simple query and count', function() {
-    beforeEach(function() {
+  describe('simple query and count', () => {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -39,19 +38,19 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 1 series', function() {
-      expect(result.data.length).to.be(1);
-      expect(result.data[0].target).to.be('Count');
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].datapoints[0][0]).to.be(10);
-      expect(result.data[0].datapoints[0][1]).to.be(1000);
+    it('should return 1 series', () => {
+      expect(result.data.length).toBe(1);
+      expect(result.data[0].target).toBe('Count');
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].datapoints[0][0]).toBe(10);
+      expect(result.data[0].datapoints[0][1]).toBe(1000);
     });
     });
   });
   });
 
 
-  describe('simple query count & avg aggregation', function() {
+  describe('simple query count & avg aggregation', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -85,22 +84,22 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(2);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].datapoints[0][0]).to.be(10);
-      expect(result.data[0].datapoints[0][1]).to.be(1000);
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(2);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].datapoints[0][0]).toBe(10);
+      expect(result.data[0].datapoints[0][1]).toBe(1000);
 
 
-      expect(result.data[1].target).to.be('Average value');
-      expect(result.data[1].datapoints[0][0]).to.be(88);
-      expect(result.data[1].datapoints[1][0]).to.be(99);
+      expect(result.data[1].target).toBe('Average value');
+      expect(result.data[1].datapoints[0][0]).toBe(88);
+      expect(result.data[1].datapoints[1][0]).toBe(99);
     });
     });
   });
   });
 
 
-  describe('single group by query one metric', function() {
+  describe('single group by query one metric', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -141,18 +140,18 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(2);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].target).to.be('server1');
-      expect(result.data[1].target).to.be('server2');
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(2);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('server1');
+      expect(result.data[1].target).toBe('server2');
     });
     });
   });
   });
 
 
-  describe('single group by query two metrics', function() {
+  describe('single group by query two metrics', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -199,20 +198,20 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(4);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].target).to.be('server1 Count');
-      expect(result.data[1].target).to.be('server1 Average @value');
-      expect(result.data[2].target).to.be('server2 Count');
-      expect(result.data[3].target).to.be('server2 Average @value');
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(4);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('server1 Count');
+      expect(result.data[1].target).toBe('server1 Average @value');
+      expect(result.data[2].target).toBe('server2 Count');
+      expect(result.data[3].target).toBe('server2 Average @value');
     });
     });
   });
   });
 
 
-  describe('with percentiles ', function() {
+  describe('with percentiles ', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -246,21 +245,21 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(2);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].target).to.be('p75');
-      expect(result.data[1].target).to.be('p90');
-      expect(result.data[0].datapoints[0][0]).to.be(3.3);
-      expect(result.data[0].datapoints[0][1]).to.be(1000);
-      expect(result.data[1].datapoints[1][0]).to.be(4.5);
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(2);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('p75');
+      expect(result.data[1].target).toBe('p90');
+      expect(result.data[0].datapoints[0][0]).toBe(3.3);
+      expect(result.data[0].datapoints[0][1]).toBe(1000);
+      expect(result.data[1].datapoints[1][0]).toBe(4.5);
     });
     });
   });
   });
 
 
-  describe('with extended_stats', function() {
+  describe('with extended_stats', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -322,21 +321,21 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 4 series', function() {
-      expect(result.data.length).to.be(4);
-      expect(result.data[0].datapoints.length).to.be(1);
-      expect(result.data[0].target).to.be('server1 Max');
-      expect(result.data[1].target).to.be('server1 Std Dev Upper');
+    it('should return 4 series', () => {
+      expect(result.data.length).toBe(4);
+      expect(result.data[0].datapoints.length).toBe(1);
+      expect(result.data[0].target).toBe('server1 Max');
+      expect(result.data[1].target).toBe('server1 Std Dev Upper');
 
 
-      expect(result.data[0].datapoints[0][0]).to.be(10.2);
-      expect(result.data[1].datapoints[0][0]).to.be(3);
+      expect(result.data[0].datapoints[0][0]).toBe(10.2);
+      expect(result.data[1].datapoints[0][0]).toBe(3);
     });
     });
   });
   });
 
 
-  describe('single group by with alias pattern', function() {
+  describe('single group by with alias pattern', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -385,19 +384,19 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(3);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].target).to.be('server1 Count and {{not_exist}} server1');
-      expect(result.data[1].target).to.be('server2 Count and {{not_exist}} server2');
-      expect(result.data[2].target).to.be('0 Count and {{not_exist}} 0');
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(3);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('server1 Count and {{not_exist}} server1');
+      expect(result.data[1].target).toBe('server2 Count and {{not_exist}} server2');
+      expect(result.data[2].target).toBe('0 Count and {{not_exist}} 0');
     });
     });
   });
   });
 
 
-  describe('histogram response', function() {
+  describe('histogram response', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -420,16 +419,16 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return table with byte and count', function() {
-      expect(result.data[0].rows.length).to.be(3);
-      expect(result.data[0].columns).to.eql([{ text: 'bytes', filterable: true }, { text: 'Count' }]);
+    it('should return table with byte and count', () => {
+      expect(result.data[0].rows.length).toBe(3);
+      expect(result.data[0].columns).toEqual([{ text: 'bytes', filterable: true }, { text: 'Count' }]);
     });
     });
   });
   });
 
 
-  describe('with two filters agg', function() {
+  describe('with two filters agg', () => {
     var result;
     var result;
 
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -472,16 +471,16 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return 2 series', function() {
-      expect(result.data.length).to.be(2);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].target).to.be('@metric:cpu');
-      expect(result.data[1].target).to.be('@metric:logins.count');
+    it('should return 2 series', () => {
+      expect(result.data.length).toBe(2);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].target).toBe('@metric:cpu');
+      expect(result.data[1].target).toBe('@metric:logins.count');
     });
     });
   });
   });
 
 
-  describe('with dropfirst and last aggregation', function() {
-    beforeEach(function() {
+  describe('with dropfirst and last aggregation', () => {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -528,14 +527,14 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should remove first and last value', function() {
-      expect(result.data.length).to.be(2);
-      expect(result.data[0].datapoints.length).to.be(1);
+    it('should remove first and last value', () => {
+      expect(result.data.length).toBe(2);
+      expect(result.data[0].datapoints.length).toBe(1);
     });
     });
   });
   });
 
 
-  describe('No group by time', function() {
-    beforeEach(function() {
+  describe('No group by time', () => {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -570,21 +569,21 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return table', function() {
-      expect(result.data.length).to.be(1);
-      expect(result.data[0].type).to.be('table');
-      expect(result.data[0].rows.length).to.be(2);
-      expect(result.data[0].rows[0][0]).to.be('server-1');
-      expect(result.data[0].rows[0][1]).to.be(1000);
-      expect(result.data[0].rows[0][2]).to.be(369);
+    it('should return table', () => {
+      expect(result.data.length).toBe(1);
+      expect(result.data[0].type).toBe('table');
+      expect(result.data[0].rows.length).toBe(2);
+      expect(result.data[0].rows[0][0]).toBe('server-1');
+      expect(result.data[0].rows[0][1]).toBe(1000);
+      expect(result.data[0].rows[0][2]).toBe(369);
 
 
-      expect(result.data[0].rows[1][0]).to.be('server-2');
-      expect(result.data[0].rows[1][1]).to.be(2000);
+      expect(result.data[0].rows[1][0]).toBe('server-2');
+      expect(result.data[0].rows[1][1]).toBe(2000);
     });
     });
   });
   });
 
 
-  describe('Multiple metrics of same type', function() {
-    beforeEach(function() {
+  describe('Multiple metrics of same type', () => {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -615,15 +614,15 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should include field in metric name', function() {
-      expect(result.data[0].type).to.be('table');
-      expect(result.data[0].rows[0][1]).to.be(1000);
-      expect(result.data[0].rows[0][2]).to.be(3000);
+    it('should include field in metric name', () => {
+      expect(result.data[0].type).toBe('table');
+      expect(result.data[0].rows[0][1]).toBe(1000);
+      expect(result.data[0].rows[0][2]).toBe(3000);
     });
     });
   });
   });
 
 
-  describe('Raw documents query', function() {
-    beforeEach(function() {
+  describe('Raw documents query', () => {
+    beforeEach(() => {
       targets = [
       targets = [
         {
         {
           refId: 'A',
           refId: 'A',
@@ -657,13 +656,13 @@ describe('ElasticResponse', function() {
       result = new ElasticResponse(targets, response).getTimeSeries();
       result = new ElasticResponse(targets, response).getTimeSeries();
     });
     });
 
 
-    it('should return docs', function() {
-      expect(result.data.length).to.be(1);
-      expect(result.data[0].type).to.be('docs');
-      expect(result.data[0].total).to.be(100);
-      expect(result.data[0].datapoints.length).to.be(2);
-      expect(result.data[0].datapoints[0].sourceProp).to.be('asd');
-      expect(result.data[0].datapoints[0].fieldProp).to.be('field');
+    it('should return docs', () => {
+      expect(result.data.length).toBe(1);
+      expect(result.data[0].type).toBe('docs');
+      expect(result.data[0].total).toBe(100);
+      expect(result.data[0].datapoints.length).toBe(2);
+      expect(result.data[0].datapoints[0].sourceProp).toBe('asd');
+      expect(result.data[0].datapoints[0].fieldProp).toBe('field');
     });
     });
   });
   });
 });
 });

+ 11 - 12
public/app/plugins/datasource/elasticsearch/specs/index_pattern_specs.ts → public/app/plugins/datasource/elasticsearch/specs/index_pattern.jest.ts

@@ -1,38 +1,37 @@
 ///<amd-dependency path="test/specs/helpers" name="helpers" />
 ///<amd-dependency path="test/specs/helpers" name="helpers" />
 
 
-import { describe, it, expect } from 'test/lib/common';
 import moment from 'moment';
 import moment from 'moment';
 import { IndexPattern } from '../index_pattern';
 import { IndexPattern } from '../index_pattern';
 
 
-describe('IndexPattern', function() {
-  describe('when getting index for today', function() {
-    it('should return correct index name', function() {
+describe('IndexPattern', () => {
+  describe('when getting index for today', () => {
+    test('should return correct index name', () => {
       var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
       var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
       var expected = 'asd-' + moment.utc().format('YYYY.MM.DD');
       var expected = 'asd-' + moment.utc().format('YYYY.MM.DD');
 
 
-      expect(pattern.getIndexForToday()).to.be(expected);
+      expect(pattern.getIndexForToday()).toBe(expected);
     });
     });
   });
   });
 
 
-  describe('when getting index list for time range', function() {
-    describe('no interval', function() {
-      it('should return correct index', function() {
+  describe('when getting index list for time range', () => {
+    describe('no interval', () => {
+      test('should return correct index', () => {
         var pattern = new IndexPattern('my-metrics', null);
         var pattern = new IndexPattern('my-metrics', null);
         var from = new Date(2015, 4, 30, 1, 2, 3);
         var from = new Date(2015, 4, 30, 1, 2, 3);
         var to = new Date(2015, 5, 1, 12, 5, 6);
         var to = new Date(2015, 5, 1, 12, 5, 6);
-        expect(pattern.getIndexList(from, to)).to.eql('my-metrics');
+        expect(pattern.getIndexList(from, to)).toEqual('my-metrics');
       });
       });
     });
     });
 
 
-    describe('daily', function() {
-      it('should return correct index list', function() {
+    describe('daily', () => {
+      test('should return correct index list', () => {
         var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
         var pattern = new IndexPattern('[asd-]YYYY.MM.DD', 'Daily');
         var from = new Date(1432940523000);
         var from = new Date(1432940523000);
         var to = new Date(1433153106000);
         var to = new Date(1433153106000);
 
 
         var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01'];
         var expected = ['asd-2015.05.29', 'asd-2015.05.30', 'asd-2015.05.31', 'asd-2015.06.01'];
 
 
-        expect(pattern.getIndexList(from, to)).to.eql(expected);
+        expect(pattern.getIndexList(from, to)).toEqual(expected);
       });
       });
     });
     });
   });
   });

+ 57 - 58
public/app/plugins/datasource/elasticsearch/specs/query_builder_specs.ts → public/app/plugins/datasource/elasticsearch/specs/query_builder.jest.ts

@@ -1,25 +1,24 @@
-import { describe, beforeEach, it, expect } from 'test/lib/common';
 import { ElasticQueryBuilder } from '../query_builder';
 import { ElasticQueryBuilder } from '../query_builder';
 
 
-describe('ElasticQueryBuilder', function() {
+describe('ElasticQueryBuilder', () => {
   var builder;
   var builder;
 
 
-  beforeEach(function() {
+  beforeEach(() => {
     builder = new ElasticQueryBuilder({ timeField: '@timestamp' });
     builder = new ElasticQueryBuilder({ timeField: '@timestamp' });
   });
   });
 
 
-  it('with defaults', function() {
+  it('with defaults', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ type: 'Count', id: '0' }],
       metrics: [{ type: 'Count', id: '0' }],
       timeField: '@timestamp',
       timeField: '@timestamp',
       bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
       bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
     });
     });
 
 
-    expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom');
-    expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom');
+    expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$timeFrom');
+    expect(query.aggs['1'].date_histogram.extended_bounds.min).toBe('$timeFrom');
   });
   });
 
 
-  it('with defaults on es5.x', function() {
+  it('with defaults on es5.x', () => {
     var builder_5x = new ElasticQueryBuilder({
     var builder_5x = new ElasticQueryBuilder({
       timeField: '@timestamp',
       timeField: '@timestamp',
       esVersion: 5,
       esVersion: 5,
@@ -31,11 +30,11 @@ describe('ElasticQueryBuilder', function() {
       bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
       bucketAggs: [{ type: 'date_histogram', field: '@timestamp', id: '1' }],
     });
     });
 
 
-    expect(query.query.bool.filter[0].range['@timestamp'].gte).to.be('$timeFrom');
-    expect(query.aggs['1'].date_histogram.extended_bounds.min).to.be('$timeFrom');
+    expect(query.query.bool.filter[0].range['@timestamp'].gte).toBe('$timeFrom');
+    expect(query.aggs['1'].date_histogram.extended_bounds.min).toBe('$timeFrom');
   });
   });
 
 
-  it('with multiple bucket aggs', function() {
+  it('with multiple bucket aggs', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ type: 'count', id: '1' }],
       metrics: [{ type: 'count', id: '1' }],
       timeField: '@timestamp',
       timeField: '@timestamp',
@@ -45,11 +44,11 @@ describe('ElasticQueryBuilder', function() {
       ],
       ],
     });
     });
 
 
-    expect(query.aggs['2'].terms.field).to.be('@host');
-    expect(query.aggs['2'].aggs['3'].date_histogram.field).to.be('@timestamp');
+    expect(query.aggs['2'].terms.field).toBe('@host');
+    expect(query.aggs['2'].aggs['3'].date_histogram.field).toBe('@timestamp');
   });
   });
 
 
-  it('with select field', function() {
+  it('with select field', () => {
     var query = builder.build(
     var query = builder.build(
       {
       {
         metrics: [{ type: 'avg', field: '@value', id: '1' }],
         metrics: [{ type: 'avg', field: '@value', id: '1' }],
@@ -60,10 +59,10 @@ describe('ElasticQueryBuilder', function() {
     );
     );
 
 
     var aggs = query.aggs['2'].aggs;
     var aggs = query.aggs['2'].aggs;
-    expect(aggs['1'].avg.field).to.be('@value');
+    expect(aggs['1'].avg.field).toBe('@value');
   });
   });
 
 
-  it('with term agg and order by metric agg', function() {
+  it('with term agg and order by metric agg', () => {
     var query = builder.build(
     var query = builder.build(
       {
       {
         metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
         metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
@@ -84,11 +83,11 @@ describe('ElasticQueryBuilder', function() {
     var firstLevel = query.aggs['2'];
     var firstLevel = query.aggs['2'];
     var secondLevel = firstLevel.aggs['3'];
     var secondLevel = firstLevel.aggs['3'];
 
 
-    expect(firstLevel.aggs['5'].avg.field).to.be('@value');
-    expect(secondLevel.aggs['5'].avg.field).to.be('@value');
+    expect(firstLevel.aggs['5'].avg.field).toBe('@value');
+    expect(secondLevel.aggs['5'].avg.field).toBe('@value');
   });
   });
 
 
-  it('with metric percentiles', function() {
+  it('with metric percentiles', () => {
     var query = builder.build(
     var query = builder.build(
       {
       {
         metrics: [
         metrics: [
@@ -109,11 +108,11 @@ describe('ElasticQueryBuilder', function() {
 
 
     var firstLevel = query.aggs['3'];
     var firstLevel = query.aggs['3'];
 
 
-    expect(firstLevel.aggs['1'].percentiles.field).to.be('@load_time');
-    expect(firstLevel.aggs['1'].percentiles.percents).to.eql([1, 2, 3, 4]);
+    expect(firstLevel.aggs['1'].percentiles.field).toBe('@load_time');
+    expect(firstLevel.aggs['1'].percentiles.percents).toEqual([1, 2, 3, 4]);
   });
   });
 
 
-  it('with filters aggs', function() {
+  it('with filters aggs', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ type: 'count', id: '1' }],
       metrics: [{ type: 'count', id: '1' }],
       timeField: '@timestamp',
       timeField: '@timestamp',
@@ -129,12 +128,12 @@ describe('ElasticQueryBuilder', function() {
       ],
       ],
     });
     });
 
 
-    expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).to.be('@metric:cpu');
-    expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).to.be('@metric:logins.count');
-    expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp');
+    expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).toBe('@metric:cpu');
+    expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).toBe('@metric:logins.count');
+    expect(query.aggs['2'].aggs['4'].date_histogram.field).toBe('@timestamp');
   });
   });
 
 
-  it('with filters aggs on es5.x', function() {
+  it('with filters aggs on es5.x', () => {
     var builder_5x = new ElasticQueryBuilder({
     var builder_5x = new ElasticQueryBuilder({
       timeField: '@timestamp',
       timeField: '@timestamp',
       esVersion: 5,
       esVersion: 5,
@@ -154,31 +153,31 @@ describe('ElasticQueryBuilder', function() {
       ],
       ],
     });
     });
 
 
-    expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).to.be('@metric:cpu');
-    expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).to.be('@metric:logins.count');
-    expect(query.aggs['2'].aggs['4'].date_histogram.field).to.be('@timestamp');
+    expect(query.aggs['2'].filters.filters['@metric:cpu'].query_string.query).toBe('@metric:cpu');
+    expect(query.aggs['2'].filters.filters['@metric:logins.count'].query_string.query).toBe('@metric:logins.count');
+    expect(query.aggs['2'].aggs['4'].date_histogram.field).toBe('@timestamp');
   });
   });
 
 
-  it('with raw_document metric', function() {
+  it('with raw_document metric', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ type: 'raw_document', id: '1', settings: {} }],
       metrics: [{ type: 'raw_document', id: '1', settings: {} }],
       timeField: '@timestamp',
       timeField: '@timestamp',
       bucketAggs: [],
       bucketAggs: [],
     });
     });
 
 
-    expect(query.size).to.be(500);
+    expect(query.size).toBe(500);
   });
   });
-  it('with raw_document metric size set', function() {
+  it('with raw_document metric size set', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }],
       metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }],
       timeField: '@timestamp',
       timeField: '@timestamp',
       bucketAggs: [],
       bucketAggs: [],
     });
     });
 
 
-    expect(query.size).to.be(1337);
+    expect(query.size).toBe(1337);
   });
   });
 
 
-  it('with moving average', function() {
+  it('with moving average', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [
       metrics: [
         {
         {
@@ -198,12 +197,12 @@ describe('ElasticQueryBuilder', function() {
 
 
     var firstLevel = query.aggs['3'];
     var firstLevel = query.aggs['3'];
 
 
-    expect(firstLevel.aggs['2']).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3');
+    expect(firstLevel.aggs['2']).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].moving_avg.buckets_path).toBe('3');
   });
   });
 
 
-  it('with broken moving average', function() {
+  it('with broken moving average', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [
       metrics: [
         {
         {
@@ -227,13 +226,13 @@ describe('ElasticQueryBuilder', function() {
 
 
     var firstLevel = query.aggs['3'];
     var firstLevel = query.aggs['3'];
 
 
-    expect(firstLevel.aggs['2']).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].moving_avg).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].moving_avg.buckets_path).to.be('3');
-    expect(firstLevel.aggs['4']).to.be(undefined);
+    expect(firstLevel.aggs['2']).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].moving_avg).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].moving_avg.buckets_path).toBe('3');
+    expect(firstLevel.aggs['4']).toBe(undefined);
   });
   });
 
 
-  it('with derivative', function() {
+  it('with derivative', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [
       metrics: [
         {
         {
@@ -252,12 +251,12 @@ describe('ElasticQueryBuilder', function() {
 
 
     var firstLevel = query.aggs['3'];
     var firstLevel = query.aggs['3'];
 
 
-    expect(firstLevel.aggs['2']).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].derivative).not.to.be(undefined);
-    expect(firstLevel.aggs['2'].derivative.buckets_path).to.be('3');
+    expect(firstLevel.aggs['2']).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].derivative).not.toBe(undefined);
+    expect(firstLevel.aggs['2'].derivative.buckets_path).toBe('3');
   });
   });
 
 
-  it('with histogram', function() {
+  it('with histogram', () => {
     var query = builder.build({
     var query = builder.build({
       metrics: [{ id: '1', type: 'count' }],
       metrics: [{ id: '1', type: 'count' }],
       bucketAggs: [
       bucketAggs: [
@@ -271,13 +270,13 @@ describe('ElasticQueryBuilder', function() {
     });
     });
 
 
     var firstLevel = query.aggs['3'];
     var firstLevel = query.aggs['3'];
-    expect(firstLevel.histogram.field).to.be('bytes');
-    expect(firstLevel.histogram.interval).to.be(10);
-    expect(firstLevel.histogram.min_doc_count).to.be(2);
-    expect(firstLevel.histogram.missing).to.be(5);
+    expect(firstLevel.histogram.field).toBe('bytes');
+    expect(firstLevel.histogram.interval).toBe(10);
+    expect(firstLevel.histogram.min_doc_count).toBe(2);
+    expect(firstLevel.histogram.missing).toBe(5);
   });
   });
 
 
-  it('with adhoc filters', function() {
+  it('with adhoc filters', () => {
     var query = builder.build(
     var query = builder.build(
       {
       {
         metrics: [{ type: 'Count', id: '0' }],
         metrics: [{ type: 'Count', id: '0' }],
@@ -295,12 +294,12 @@ describe('ElasticQueryBuilder', function() {
       ]
       ]
     );
     );
 
 
-    expect(query.query.bool.must[0].match_phrase['key1'].query).to.be('value1');
-    expect(query.query.bool.must[1].match_phrase['key2'].query).to.be('value2');
-    expect(query.query.bool.must_not[0].match_phrase['key2'].query).to.be('value2');
-    expect(query.query.bool.filter[2].range['key3'].lt).to.be('value3');
-    expect(query.query.bool.filter[3].range['key4'].gt).to.be('value4');
-    expect(query.query.bool.filter[4].regexp['key5']).to.be('value5');
-    expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).to.be('value6');
+    expect(query.query.bool.must[0].match_phrase['key1'].query).toBe('value1');
+    expect(query.query.bool.must[1].match_phrase['key2'].query).toBe('value2');
+    expect(query.query.bool.must_not[0].match_phrase['key2'].query).toBe('value2');
+    expect(query.query.bool.filter[2].range['key3'].lt).toBe('value3');
+    expect(query.query.bool.filter[3].range['key4'].gt).toBe('value4');
+    expect(query.query.bool.filter[4].regexp['key5']).toBe('value5');
+    expect(query.query.bool.filter[5].bool.must_not.regexp['key6']).toBe('value6');
   });
   });
 });
 });

+ 93 - 0
public/app/plugins/datasource/elasticsearch/specs/query_def.jest.ts

@@ -0,0 +1,93 @@
+import * as queryDef from '../query_def';
+
+describe('ElasticQueryDef', () => {
+  describe('getPipelineAggOptions', () => {
+    describe('with zero targets', () => {
+      var response = queryDef.getPipelineAggOptions([]);
+
+      test('should return zero', () => {
+        expect(response.length).toBe(0);
+      });
+    });
+
+    describe('with count and sum targets', () => {
+      var targets = {
+        metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }],
+      };
+
+      var response = queryDef.getPipelineAggOptions(targets);
+
+      test('should return zero', () => {
+        expect(response.length).toBe(2);
+      });
+    });
+
+    describe('with count and moving average targets', () => {
+      var targets = {
+        metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }],
+      };
+
+      var response = queryDef.getPipelineAggOptions(targets);
+
+      test('should return one', () => {
+        expect(response.length).toBe(1);
+      });
+    });
+
+    describe('with derivatives targets', () => {
+      var targets = {
+        metrics: [{ type: 'derivative', field: '@value' }],
+      };
+
+      var response = queryDef.getPipelineAggOptions(targets);
+
+      test('should return zero', () => {
+        expect(response.length).toBe(0);
+      });
+    });
+  });
+
+  describe('isPipelineMetric', () => {
+    describe('moving_avg', () => {
+      var result = queryDef.isPipelineAgg('moving_avg');
+
+      test('is pipe line metric', () => {
+        expect(result).toBe(true);
+      });
+    });
+
+    describe('count', () => {
+      var result = queryDef.isPipelineAgg('count');
+
+      test('is not pipe line metric', () => {
+        expect(result).toBe(false);
+      });
+    });
+  });
+
+  describe('pipeline aggs depending on esverison', () => {
+    describe('using esversion undefined', () => {
+      test('should not get pipeline aggs', () => {
+        expect(queryDef.getMetricAggTypes(undefined).length).toBe(9);
+      });
+    });
+
+    describe('using esversion 1', () => {
+      test('should not get pipeline aggs', () => {
+        expect(queryDef.getMetricAggTypes(1).length).toBe(9);
+      });
+    });
+
+    describe('using esversion 2', () => {
+      test('should get pipeline aggs', () => {
+        expect(queryDef.getMetricAggTypes(2).length).toBe(11);
+      });
+    });
+
+    describe('using esversion 5', () => {
+      test('should get pipeline aggs', () => {
+        expect(queryDef.getMetricAggTypes(5).length).toBe(11);
+      });
+    });
+  });
+});

+ 0 - 95
public/app/plugins/datasource/elasticsearch/specs/query_def_specs.ts

@@ -1,95 +0,0 @@
-import { describe, it, expect } from 'test/lib/common';
-
-import * as queryDef from '../query_def';
-
-describe('ElasticQueryDef', function() {
-  describe('getPipelineAggOptions', function() {
-    describe('with zero targets', function() {
-      var response = queryDef.getPipelineAggOptions([]);
-
-      it('should return zero', function() {
-        expect(response.length).to.be(0);
-      });
-    });
-
-    describe('with count and sum targets', function() {
-      var targets = {
-        metrics: [{ type: 'count', field: '@value' }, { type: 'sum', field: '@value' }],
-      };
-
-      var response = queryDef.getPipelineAggOptions(targets);
-
-      it('should return zero', function() {
-        expect(response.length).to.be(2);
-      });
-    });
-
-    describe('with count and moving average targets', function() {
-      var targets = {
-        metrics: [{ type: 'count', field: '@value' }, { type: 'moving_avg', field: '@value' }],
-      };
-
-      var response = queryDef.getPipelineAggOptions(targets);
-
-      it('should return one', function() {
-        expect(response.length).to.be(1);
-      });
-    });
-
-    describe('with derivatives targets', function() {
-      var targets = {
-        metrics: [{ type: 'derivative', field: '@value' }],
-      };
-
-      var response = queryDef.getPipelineAggOptions(targets);
-
-      it('should return zero', function() {
-        expect(response.length).to.be(0);
-      });
-    });
-  });
-
-  describe('isPipelineMetric', function() {
-    describe('moving_avg', function() {
-      var result = queryDef.isPipelineAgg('moving_avg');
-
-      it('is pipe line metric', function() {
-        expect(result).to.be(true);
-      });
-    });
-
-    describe('count', function() {
-      var result = queryDef.isPipelineAgg('count');
-
-      it('is not pipe line metric', function() {
-        expect(result).to.be(false);
-      });
-    });
-  });
-
-  describe('pipeline aggs depending on esverison', function() {
-    describe('using esversion undefined', function() {
-      it('should not get pipeline aggs', function() {
-        expect(queryDef.getMetricAggTypes(undefined).length).to.be(9);
-      });
-    });
-
-    describe('using esversion 1', function() {
-      it('should not get pipeline aggs', function() {
-        expect(queryDef.getMetricAggTypes(1).length).to.be(9);
-      });
-    });
-
-    describe('using esversion 2', function() {
-      it('should get pipeline aggs', function() {
-        expect(queryDef.getMetricAggTypes(2).length).to.be(11);
-      });
-    });
-
-    describe('using esversion 5', function() {
-      it('should get pipeline aggs', function() {
-        expect(queryDef.getMetricAggTypes(5).length).to.be(11);
-      });
-    });
-  });
-});

+ 0 - 1
public/app/plugins/datasource/influxdb/specs/query_builder.jest.ts

@@ -1,4 +1,3 @@
-import { describe, it, expect } from 'test/lib/common';
 import { InfluxQueryBuilder } from '../query_builder';
 import { InfluxQueryBuilder } from '../query_builder';
 
 
 describe('InfluxQueryBuilder', function() {
 describe('InfluxQueryBuilder', function() {

+ 4 - 3
public/app/plugins/panel/dashlist/editor.html

@@ -23,10 +23,11 @@
     </div>
     </div>
 
 
     <div class="gf-form">
     <div class="gf-form">
-      <folder-picker  root-name="All"
-                      initial-folder-id="ctrl.panel.folderId"
+      <folder-picker  initial-folder-id="ctrl.panel.folderId"
 											on-change="ctrl.onFolderChange($folder)"
 											on-change="ctrl.onFolderChange($folder)"
-											label-class="width-6">
+                      label-class="width-6"
+                      initial-title="'All'"
+                      enable-reset="true">
 			</folder-picker>
 			</folder-picker>
     </div>
     </div>
 
 

+ 3 - 2
public/app/plugins/panel/dashlist/module.ts

@@ -17,7 +17,7 @@ class DashListCtrl extends PanelCtrl {
     search: false,
     search: false,
     starred: true,
     starred: true,
     headings: true,
     headings: true,
-    folderId: 0,
+    folderId: null,
   };
   };
 
 
   /** @ngInject */
   /** @ngInject */
@@ -85,7 +85,8 @@ class DashListCtrl extends PanelCtrl {
       limit: this.panel.limit,
       limit: this.panel.limit,
       query: this.panel.query,
       query: this.panel.query,
       tag: this.panel.tags,
       tag: this.panel.tags,
-      folderId: this.panel.folderId,
+      folderIds: this.panel.folderId,
+      type: 'dash-db',
     };
     };
 
 
     return this.backendSrv.search(params).then(result => {
     return this.backendSrv.search(params).then(result => {

+ 15 - 15
public/app/plugins/panel/graph/specs/tooltip_specs.ts → public/app/plugins/panel/graph/specs/graph_tooltip.jest.ts

@@ -1,11 +1,11 @@
-import { describe, beforeEach, it, sinon, expect } from '../../../../../test/lib/common';
+jest.mock('app/core/core', () => ({}));
 
 
 import $ from 'jquery';
 import $ from 'jquery';
 import GraphTooltip from '../graph_tooltip';
 import GraphTooltip from '../graph_tooltip';
 
 
 var scope = {
 var scope = {
-  appEvent: sinon.spy(),
-  onAppEvent: sinon.spy(),
+  appEvent: jest.fn(),
+  onAppEvent: jest.fn(),
   ctrl: {},
   ctrl: {},
 };
 };
 
 
@@ -47,22 +47,22 @@ describe('findHoverIndexFromData', function() {
 
 
   it('should return 0 if posX out of lower bounds', function() {
   it('should return 0 if posX out of lower bounds', function() {
     var posX = 99;
     var posX = 99;
-    expect(tooltip.findHoverIndexFromData(posX, series)).to.be(0);
+    expect(tooltip.findHoverIndexFromData(posX, series)).toBe(0);
   });
   });
 
 
   it('should return n - 1 if posX out of upper bounds', function() {
   it('should return n - 1 if posX out of upper bounds', function() {
     var posX = 108;
     var posX = 108;
-    expect(tooltip.findHoverIndexFromData(posX, series)).to.be(series.data.length - 1);
+    expect(tooltip.findHoverIndexFromData(posX, series)).toBe(series.data.length - 1);
   });
   });
 
 
   it('should return i if posX in series', function() {
   it('should return i if posX in series', function() {
     var posX = 104;
     var posX = 104;
-    expect(tooltip.findHoverIndexFromData(posX, series)).to.be(4);
+    expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4);
   });
   });
 
 
   it('should return i if posX not in series and i + 1 > posX', function() {
   it('should return i if posX not in series and i + 1 > posX', function() {
     var posX = 104.9;
     var posX = 104.9;
-    expect(tooltip.findHoverIndexFromData(posX, series)).to.be(4);
+    expect(tooltip.findHoverIndexFromData(posX, series)).toBe(4);
   });
   });
 });
 });
 
 
@@ -73,17 +73,17 @@ describeSharedTooltip('steppedLine false, stack false', function(ctx) {
   });
   });
 
 
   it('should return 2 series', function() {
   it('should return 2 series', function() {
-    expect(ctx.results.length).to.be(2);
+    expect(ctx.results.length).toBe(2);
   });
   });
 
 
   it('should add time to results array', function() {
   it('should add time to results array', function() {
-    expect(ctx.results.time).to.be(10);
+    expect(ctx.results.time).toBe(10);
   });
   });
 
 
   it('should set value and hoverIndex', function() {
   it('should set value and hoverIndex', function() {
-    expect(ctx.results[0].value).to.be(15);
-    expect(ctx.results[1].value).to.be(2);
-    expect(ctx.results[0].hoverIndex).to.be(0);
+    expect(ctx.results[0].value).toBe(15);
+    expect(ctx.results[1].value).toBe(2);
+    expect(ctx.results[0].hoverIndex).toBe(0);
   });
   });
 });
 });
 
 
@@ -121,7 +121,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false', functio
   });
   });
 
 
   it('should show stacked value', function() {
   it('should show stacked value', function() {
-    expect(ctx.results[1].value).to.be(17);
+    expect(ctx.results[1].value).toBe(17);
   });
   });
 });
 });
 
 
@@ -152,7 +152,7 @@ describeSharedTooltip('steppedLine false, stack true, individual false, series s
   });
   });
 
 
   it('should not show stacked value', function() {
   it('should not show stacked value', function() {
-    expect(ctx.results[1].value).to.be(2);
+    expect(ctx.results[1].value).toBe(2);
   });
   });
 });
 });
 
 
@@ -184,6 +184,6 @@ describeSharedTooltip('steppedLine false, stack true, individual true', function
   });
   });
 
 
   it('should not show stacked value', function() {
   it('should not show stacked value', function() {
-    expect(ctx.results[1].value).to.be(2);
+    expect(ctx.results[1].value).toBe(2);
   });
   });
 });
 });

+ 20 - 22
public/app/plugins/panel/graph/specs/threshold_manager_specs.ts → public/app/plugins/panel/graph/specs/threshold_manager.jest.ts

@@ -1,5 +1,3 @@
-import { describe, it, expect } from '../../../../../test/lib/common';
-
 import angular from 'angular';
 import angular from 'angular';
 import TimeSeries from 'app/core/time_series2';
 import TimeSeries from 'app/core/time_series2';
 import { ThresholdManager } from '../threshold_manager';
 import { ThresholdManager } from '../threshold_manager';
@@ -38,16 +36,16 @@ describe('ThresholdManager', function() {
       it('should add fill for threshold with fill: true', function() {
       it('should add fill for threshold with fill: true', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
 
 
-        expect(markings[0].yaxis.from).to.be(300);
-        expect(markings[0].yaxis.to).to.be(Infinity);
-        expect(markings[0].color).to.be('rgba(234, 112, 112, 0.12)');
+        expect(markings[0].yaxis.from).toBe(300);
+        expect(markings[0].yaxis.to).toBe(Infinity);
+        expect(markings[0].color).toBe('rgba(234, 112, 112, 0.12)');
       });
       });
 
 
       it('should add line', function() {
       it('should add line', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[1].yaxis.from).to.be(300);
-        expect(markings[1].yaxis.to).to.be(300);
-        expect(markings[1].color).to.be('rgba(237, 46, 24, 0.60)');
+        expect(markings[1].yaxis.from).toBe(300);
+        expect(markings[1].yaxis.to).toBe(300);
+        expect(markings[1].color).toBe('rgba(237, 46, 24, 0.60)');
       });
       });
     });
     });
 
 
@@ -59,14 +57,14 @@ describe('ThresholdManager', function() {
 
 
       it('should add fill for first thresholds to next threshold', function() {
       it('should add fill for first thresholds to next threshold', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[0].yaxis.from).to.be(200);
-        expect(markings[0].yaxis.to).to.be(300);
+        expect(markings[0].yaxis.from).toBe(200);
+        expect(markings[0].yaxis.to).toBe(300);
       });
       });
 
 
       it('should add fill for last thresholds to infinity', function() {
       it('should add fill for last thresholds to infinity', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[1].yaxis.from).to.be(300);
-        expect(markings[1].yaxis.to).to.be(Infinity);
+        expect(markings[1].yaxis.from).toBe(300);
+        expect(markings[1].yaxis.to).toBe(Infinity);
       });
       });
     });
     });
 
 
@@ -78,14 +76,14 @@ describe('ThresholdManager', function() {
 
 
       it('should add fill for first thresholds to next threshold', function() {
       it('should add fill for first thresholds to next threshold', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[0].yaxis.from).to.be(300);
-        expect(markings[0].yaxis.to).to.be(200);
+        expect(markings[0].yaxis.from).toBe(300);
+        expect(markings[0].yaxis.to).toBe(200);
       });
       });
 
 
       it('should add fill for last thresholds to itself', function() {
       it('should add fill for last thresholds to itself', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[1].yaxis.from).to.be(200);
-        expect(markings[1].yaxis.to).to.be(200);
+        expect(markings[1].yaxis.from).toBe(200);
+        expect(markings[1].yaxis.to).toBe(200);
       });
       });
     });
     });
 
 
@@ -97,14 +95,14 @@ describe('ThresholdManager', function() {
 
 
       it('should add fill for first thresholds to next threshold', function() {
       it('should add fill for first thresholds to next threshold', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[0].yaxis.from).to.be(300);
-        expect(markings[0].yaxis.to).to.be(Infinity);
+        expect(markings[0].yaxis.from).toBe(300);
+        expect(markings[0].yaxis.to).toBe(Infinity);
       });
       });
 
 
       it('should add fill for last thresholds to itself', function() {
       it('should add fill for last thresholds to itself', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[1].yaxis.from).to.be(200);
-        expect(markings[1].yaxis.to).to.be(-Infinity);
+        expect(markings[1].yaxis.from).toBe(200);
+        expect(markings[1].yaxis.to).toBe(-Infinity);
       });
       });
     });
     });
 
 
@@ -130,12 +128,12 @@ describe('ThresholdManager', function() {
 
 
       it('should add first threshold for left axis', function() {
       it('should add first threshold for left axis', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[0].yaxis.from).to.be(100);
+        expect(markings[0].yaxis.from).toBe(100);
       });
       });
 
 
       it('should add second threshold for right axis', function() {
       it('should add second threshold for right axis', function() {
         var markings = ctx.options.grid.markings;
         var markings = ctx.options.grid.markings;
-        expect(markings[1].y2axis.from).to.be(200);
+        expect(markings[1].y2axis.from).toBe(200);
       });
       });
     });
     });
   });
   });

+ 11 - 13
public/app/plugins/panel/singlestat/specs/singlestat_panel_spec.ts → public/app/plugins/panel/singlestat/specs/singlestat_panel.jest.ts

@@ -1,5 +1,3 @@
-import { describe, it, expect } from 'test/lib/common';
-
 import { getColorForValue } from '../module';
 import { getColorForValue } from '../module';
 
 
 describe('grafanaSingleStat', function() {
 describe('grafanaSingleStat', function() {
@@ -11,31 +9,31 @@ describe('grafanaSingleStat', function() {
       };
       };
 
 
       it('5 should return green', () => {
       it('5 should return green', () => {
-        expect(getColorForValue(data, 5)).to.be('green');
+        expect(getColorForValue(data, 5)).toBe('green');
       });
       });
 
 
       it('19.9 should return green', () => {
       it('19.9 should return green', () => {
-        expect(getColorForValue(data, 19.9)).to.be('green');
+        expect(getColorForValue(data, 19.9)).toBe('green');
       });
       });
 
 
       it('20 should return yellow', () => {
       it('20 should return yellow', () => {
-        expect(getColorForValue(data, 20)).to.be('yellow');
+        expect(getColorForValue(data, 20)).toBe('yellow');
       });
       });
 
 
       it('20.1 should return yellow', () => {
       it('20.1 should return yellow', () => {
-        expect(getColorForValue(data, 20.1)).to.be('yellow');
+        expect(getColorForValue(data, 20.1)).toBe('yellow');
       });
       });
 
 
       it('25 should return yellow', () => {
       it('25 should return yellow', () => {
-        expect(getColorForValue(data, 25)).to.be('yellow');
+        expect(getColorForValue(data, 25)).toBe('yellow');
       });
       });
 
 
       it('50 should return red', () => {
       it('50 should return red', () => {
-        expect(getColorForValue(data, 50)).to.be('red');
+        expect(getColorForValue(data, 50)).toBe('red');
       });
       });
 
 
       it('55 should return red', () => {
       it('55 should return red', () => {
-        expect(getColorForValue(data, 55)).to.be('red');
+        expect(getColorForValue(data, 55)).toBe('red');
       });
       });
     });
     });
   });
   });
@@ -47,15 +45,15 @@ describe('grafanaSingleStat', function() {
     };
     };
 
 
     it('-30 should return green', () => {
     it('-30 should return green', () => {
-      expect(getColorForValue(data, -30)).to.be('green');
+      expect(getColorForValue(data, -30)).toBe('green');
     });
     });
 
 
     it('1 should return green', () => {
     it('1 should return green', () => {
-      expect(getColorForValue(data, 1)).to.be('yellow');
+      expect(getColorForValue(data, 1)).toBe('yellow');
     });
     });
 
 
     it('22 should return green', () => {
     it('22 should return green', () => {
-      expect(getColorForValue(data, 22)).to.be('red');
+      expect(getColorForValue(data, 22)).toBe('red');
     });
     });
   });
   });
 
 
@@ -66,7 +64,7 @@ describe('grafanaSingleStat', function() {
     };
     };
 
 
     it('-30 should return green', () => {
     it('-30 should return green', () => {
-      expect(getColorForValue(data, -26)).to.be('yellow');
+      expect(getColorForValue(data, -26)).toBe('yellow');
     });
     });
   });
   });
 });
 });

+ 16 - 0
public/sass/components/_buttons.scss

@@ -100,6 +100,22 @@
 // Success appears as green
 // Success appears as green
 .btn-success {
 .btn-success {
   @include buttonBackground($btn-success-bg, $btn-success-bg-hl);
   @include buttonBackground($btn-success-bg, $btn-success-bg-hl);
+
+  &--processing {
+    @include button-outline-variant($gray-1);
+    @include box-shadow(none);
+    cursor: default;
+
+    &:hover,
+    &:active,
+    &:active:hover,
+    &:focus,
+    &:disabled {
+      color: $gray-1;
+      background-color: transparent;
+      border-color: $gray-1;
+    }
+  }
 }
 }
 // Info appears as a neutral blue
 // Info appears as a neutral blue
 .btn-secondary {
 .btn-secondary {

+ 12 - 4
public/sass/components/_row.scss

@@ -11,11 +11,20 @@
       display: inline-block;
       display: inline-block;
     }
     }
 
 
-    .dashboard-row__drag,
-    .dashboard-row__actions {
+    .dashboard-row__drag {
       visibility: visible;
       visibility: visible;
       opacity: 1;
       opacity: 1;
     }
     }
+
+    .dashboard-row__actions {
+      visibility: hidden;
+    }
+
+    .dashboard-row__toggle-target {
+      flex: 1;
+      cursor: pointer;
+      margin-right: 15px;
+    }
   }
   }
 
 
   &:hover {
   &:hover {
@@ -43,7 +52,6 @@
   color: $text-muted;
   color: $text-muted;
   visibility: hidden;
   visibility: hidden;
   opacity: 0;
   opacity: 0;
-  flex-grow: 1;
   transition: 200ms opacity ease-in 200ms;
   transition: 200ms opacity ease-in 200ms;
 
 
   a {
   a {
@@ -69,7 +77,7 @@
   cursor: move;
   cursor: move;
   width: 1rem;
   width: 1rem;
   height: 100%;
   height: 100%;
-  background: url("../img/grab_dark.svg") no-repeat 50% 50%;
+  background: url('../img/grab_dark.svg') no-repeat 50% 50%;
   background-size: 8px;
   background-size: 8px;
   visibility: hidden;
   visibility: hidden;
   position: absolute;
   position: absolute;

+ 1 - 1
public/sass/pages/_alerting.scss

@@ -32,7 +32,7 @@
   .panel-alert-icon:before {
   .panel-alert-icon:before {
     content: '\e611';
     content: '\e611';
     position: relative;
     position: relative;
-    top: 1px;
+    top: 5px;
     left: -3px;
     left: -3px;
   }
   }
 }
 }

+ 15 - 8
public/sass/pages/_dashboard.scss

@@ -68,17 +68,26 @@ div.flot-text {
   font-weight: $font-weight-semi-bold;
   font-weight: $font-weight-semi-bold;
   position: relative;
   position: relative;
   width: 100%;
   width: 100%;
-  display: block;
-  padding-bottom: 2px;
+  display: flex;
+  flex-wrap: nowrap;
+  justify-content: center;
+  padding: 4px 0 4px;
 }
 }
 
 
 .panel-title-text {
 .panel-title-text {
+  text-overflow: ellipsis;
+  overflow: hidden;
+  white-space: nowrap;
+  max-width: calc(100% - 38px);
   cursor: pointer;
   cursor: pointer;
   font-weight: $font-weight-semi-bold;
   font-weight: $font-weight-semi-bold;
 
 
   &:hover {
   &:hover {
     color: $link-hover-color;
     color: $link-hover-color;
   }
   }
+  .panel-has-alert & {
+    max-width: calc(100% - 54px);
+  }
 }
 }
 
 
 .panel-menu-container {
 .panel-menu-container {
@@ -97,7 +106,7 @@ div.flot-text {
   width: 16px;
   width: 16px;
   height: 16px;
   height: 16px;
   left: 1px;
   left: 1px;
-  top: 4px;
+  top: 2px;
 
 
   &:hover {
   &:hover {
     color: $link-hover-color;
     color: $link-hover-color;
@@ -114,8 +123,6 @@ div.flot-text {
 }
 }
 
 
 .panel-header {
 .panel-header {
-  text-align: center;
-
   &:hover {
   &:hover {
     transition: background-color 0.1s ease-in-out;
     transition: background-color 0.1s ease-in-out;
     background-color: $panel-header-hover-bg;
     background-color: $panel-header-hover-bg;
@@ -156,8 +163,8 @@ div.flot-text {
 
 
   .fa {
   .fa {
     position: relative;
     position: relative;
-    top: -4px;
-    left: -6px;
+    top: -2px;
+    left: 6px;
     font-size: 75%;
     font-size: 75%;
     z-index: 1;
     z-index: 1;
   }
   }
@@ -174,7 +181,7 @@ div.flot-text {
     display: block;
     display: block;
     @include panel-corner-color(lighten($panel-bg, 4%));
     @include panel-corner-color(lighten($panel-bg, 4%));
     .fa {
     .fa {
-      left: -5px;
+      left: 4px;
     }
     }
     .fa:before {
     .fa:before {
       content: '\f08e';
       content: '\f08e';

+ 13 - 3
public/sass/pages/_login.scss

@@ -13,6 +13,13 @@ $login-border: #8daac5;
   justify-content: center;
   justify-content: center;
   background-image: url(../img/heatmap_bg_test.svg);
   background-image: url(../img/heatmap_bg_test.svg);
   background-size: cover;
   background-size: cover;
+  color: #d8d9da;
+  & a {
+    color: #d8d9da !important;
+  }
+  & .btn-primary {
+    @include buttonBackground(#ff6600, #bc3e06);
+  }
 }
 }
 
 
 input:-webkit-autofill,
 input:-webkit-autofill,
@@ -25,8 +32,9 @@ textarea:-webkit-autofill:focus,
 select:-webkit-autofill,
 select:-webkit-autofill,
 select:-webkit-autofill:hover,
 select:-webkit-autofill:hover,
 select:-webkit-autofill:focus {
 select:-webkit-autofill:focus {
-  -webkit-box-shadow: 0 0 0px 1000px $black inset;
-  -webkit-text-fill-color: $gray-7 !important;
+  -webkit-box-shadow: 0 0 0px 1000px $black inset !important;
+  -webkit-text-fill-color: #fbfbfb !important;
+  box-shadow: 0 0 0px 1000px $black inset;
 }
 }
 
 
 .login-form-group {
 .login-form-group {
@@ -46,6 +54,8 @@ select:-webkit-autofill:focus {
   border: 1px solid $login-border;
   border: 1px solid $login-border;
   border-radius: 4px;
   border-radius: 4px;
   opacity: 0.6;
   opacity: 0.6;
+  background: $black;
+  color: #fbfbfb;
 
 
   &:focus {
   &:focus {
     border: 1px solid $login-border;
     border: 1px solid $login-border;
@@ -103,7 +113,7 @@ select:-webkit-autofill:focus {
   }
   }
 
 
   .icon-gf-grafana_wordmark {
   .icon-gf-grafana_wordmark {
-    color: $link-color;
+    color: darken($white, 11%);
     position: relative;
     position: relative;
     font-size: 2rem;
     font-size: 2rem;
     text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);
     text-shadow: 2px 2px 5px rgba(0, 0, 0, 0.3);

+ 6 - 8
public/test/core/utils/version_specs.ts → public/test/core/utils/version_jest.ts

@@ -1,5 +1,3 @@
-import {describe, beforeEach, it, expect} from 'test/lib/common';
-
 import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version';
 import {SemVersion, isVersionGtOrEq} from 'app/core/utils/version';
 
 
 describe("SemVersion", () => {
 describe("SemVersion", () => {
@@ -8,10 +6,10 @@ describe("SemVersion", () => {
   describe('parsing', () => {
   describe('parsing', () => {
     it('should parse version properly', () => {
     it('should parse version properly', () => {
       let semver = new SemVersion(version);
       let semver = new SemVersion(version);
-      expect(semver.major).to.be(1);
-      expect(semver.minor).to.be(0);
-      expect(semver.patch).to.be(0);
-      expect(semver.meta).to.be('alpha.1');
+      expect(semver.major).toBe(1);
+      expect(semver.minor).toBe(0);
+      expect(semver.patch).toBe(0);
+      expect(semver.meta).toBe('alpha.1');
     });
     });
   });
   });
 
 
@@ -30,7 +28,7 @@ describe("SemVersion", () => {
         {value: '3.5', expected: false},
         {value: '3.5', expected: false},
       ];
       ];
       cases.forEach((testCase) => {
       cases.forEach((testCase) => {
-        expect(semver.isGtOrEq(testCase.value)).to.be(testCase.expected);
+        expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected);
       });
       });
     });
     });
   });
   });
@@ -48,7 +46,7 @@ describe("SemVersion", () => {
         {values: ['3.4.5', '3.5'], expected: false},
         {values: ['3.4.5', '3.5'], expected: false},
       ];
       ];
       cases.forEach((testCase) => {
       cases.forEach((testCase) => {
-        expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).to.be(testCase.expected);
+        expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected);
       });
       });
     });
     });
   });
   });

+ 18 - 0
public/test/jest-setup.ts

@@ -1,4 +1,22 @@
 import { configure } from 'enzyme';
 import { configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
 import Adapter from 'enzyme-adapter-react-16';
+import 'jquery';
+import $ from 'jquery';
+import 'angular';
+import angular from 'angular';
+
+angular.module('grafana', ['ngRoute']);
+angular.module('grafana.services', ['ngRoute', '$strap.directives']);
+angular.module('grafana.panels', []);
+angular.module('grafana.controllers', []);
+angular.module('grafana.directives', []);
+angular.module('grafana.filters', []);
+angular.module('grafana.routes', ['ngRoute']);
+
+jest.mock('app/core/core', () => ({}));
+jest.mock('app/features/plugins/plugin_loader', () => ({}));
 
 
 configure({ adapter: new Adapter() });
 configure({ adapter: new Adapter() });
+
+var global = <any>window;
+global.$ = global.jQuery = $;

+ 1 - 1
scripts/webpack/webpack.hot.js

@@ -23,7 +23,7 @@ module.exports = merge(common, {
   },
   },
 
 
   resolve: {
   resolve: {
-    extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png'],
+    extensions: ['.scss', '.ts', '.tsx', '.es6', '.js', '.json', '.svg', '.woff2', '.png', '.html'],
   },
   },
 
 
   devtool: 'eval-source-map',
   devtool: 'eval-source-map',

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 531 - 106
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.c


Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 529 - 104
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.h


+ 60 - 32
vendor/github.com/mattn/go-sqlite3/sqlite3.go

@@ -1,3 +1,5 @@
+// +build cgo
+
 // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
 // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
 //
 //
 // Use of this source code is governed by an MIT-style
 // Use of this source code is governed by an MIT-style
@@ -7,10 +9,13 @@ package sqlite3
 
 
 /*
 /*
 #cgo CFLAGS: -std=gnu99
 #cgo CFLAGS: -std=gnu99
-#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE=1
+#cgo CFLAGS: -DSQLITE_ENABLE_RTREE -DSQLITE_THREADSAFE=1 -DHAVE_USLEEP=1
+#cgo linux,!android CFLAGS: -DHAVE_PREAD64=1 -DHAVE_PWRITE64=1
 #cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4_UNICODE61
 #cgo CFLAGS: -DSQLITE_ENABLE_FTS3 -DSQLITE_ENABLE_FTS3_PARENTHESIS -DSQLITE_ENABLE_FTS4_UNICODE61
 #cgo CFLAGS: -DSQLITE_TRACE_SIZE_LIMIT=15
 #cgo CFLAGS: -DSQLITE_TRACE_SIZE_LIMIT=15
+#cgo CFLAGS: -DSQLITE_OMIT_DEPRECATED
 #cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC
 #cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC
+#cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
 #cgo CFLAGS: -Wno-deprecated-declarations
 #cgo CFLAGS: -Wno-deprecated-declarations
 #ifndef USE_LIBSQLITE3
 #ifndef USE_LIBSQLITE3
 #include <sqlite3-binding.h>
 #include <sqlite3-binding.h>
@@ -170,6 +175,12 @@ var SQLiteTimestampFormats = []string{
 	"2006-01-02",
 	"2006-01-02",
 }
 }
 
 
+const (
+	columnDate      string = "date"
+	columnDatetime  string = "datetime"
+	columnTimestamp string = "timestamp"
+)
+
 func init() {
 func init() {
 	sql.Register("sqlite3", &SQLiteDriver{})
 	sql.Register("sqlite3", &SQLiteDriver{})
 }
 }
@@ -389,7 +400,7 @@ func (c *SQLiteConn) RegisterCommitHook(callback func() int) {
 	if callback == nil {
 	if callback == nil {
 		C.sqlite3_commit_hook(c.db, nil, nil)
 		C.sqlite3_commit_hook(c.db, nil, nil)
 	} else {
 	} else {
-		C.sqlite3_commit_hook(c.db, (*[0]byte)(unsafe.Pointer(C.commitHookTrampoline)), unsafe.Pointer(newHandle(c, callback)))
+		C.sqlite3_commit_hook(c.db, (*[0]byte)(C.commitHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
 	}
 	}
 }
 }
 
 
@@ -402,7 +413,7 @@ func (c *SQLiteConn) RegisterRollbackHook(callback func()) {
 	if callback == nil {
 	if callback == nil {
 		C.sqlite3_rollback_hook(c.db, nil, nil)
 		C.sqlite3_rollback_hook(c.db, nil, nil)
 	} else {
 	} else {
-		C.sqlite3_rollback_hook(c.db, (*[0]byte)(unsafe.Pointer(C.rollbackHookTrampoline)), unsafe.Pointer(newHandle(c, callback)))
+		C.sqlite3_rollback_hook(c.db, (*[0]byte)(C.rollbackHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
 	}
 	}
 }
 }
 
 
@@ -419,7 +430,7 @@ func (c *SQLiteConn) RegisterUpdateHook(callback func(int, string, string, int64
 	if callback == nil {
 	if callback == nil {
 		C.sqlite3_update_hook(c.db, nil, nil)
 		C.sqlite3_update_hook(c.db, nil, nil)
 	} else {
 	} else {
-		C.sqlite3_update_hook(c.db, (*[0]byte)(unsafe.Pointer(C.updateHookTrampoline)), unsafe.Pointer(newHandle(c, callback)))
+		C.sqlite3_update_hook(c.db, (*[0]byte)(C.updateHookTrampoline), unsafe.Pointer(newHandle(c, callback)))
 	}
 	}
 }
 }
 
 
@@ -501,7 +512,7 @@ func (c *SQLiteConn) RegisterFunc(name string, impl interface{}, pure bool) erro
 }
 }
 
 
 func sqlite3CreateFunction(db *C.sqlite3, zFunctionName *C.char, nArg C.int, eTextRep C.int, pApp uintptr, xFunc unsafe.Pointer, xStep unsafe.Pointer, xFinal unsafe.Pointer) C.int {
 func sqlite3CreateFunction(db *C.sqlite3, zFunctionName *C.char, nArg C.int, eTextRep C.int, pApp uintptr, xFunc unsafe.Pointer, xStep unsafe.Pointer, xFinal unsafe.Pointer) C.int {
-	return C._sqlite3_create_function(db, zFunctionName, nArg, eTextRep, C.uintptr_t(pApp), (*[0]byte)(unsafe.Pointer(xFunc)), (*[0]byte)(unsafe.Pointer(xStep)), (*[0]byte)(unsafe.Pointer(xFinal)))
+	return C._sqlite3_create_function(db, zFunctionName, nArg, eTextRep, C.uintptr_t(pApp), (*[0]byte)(xFunc), (*[0]byte)(xStep), (*[0]byte)(xFinal))
 }
 }
 
 
 // RegisterAggregator makes a Go type available as a SQLite aggregation function.
 // RegisterAggregator makes a Go type available as a SQLite aggregation function.
@@ -780,6 +791,8 @@ func errorString(err Error) string {
 //     Enable or disable enforcement of foreign keys.  X can be 1 or 0.
 //     Enable or disable enforcement of foreign keys.  X can be 1 or 0.
 //   _recursive_triggers=X
 //   _recursive_triggers=X
 //     Enable or disable recursive triggers.  X can be 1 or 0.
 //     Enable or disable recursive triggers.  X can be 1 or 0.
+//   _mutex=XXX
+//     Specify mutex mode. XXX can be "no", "full".
 func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 	if C.sqlite3_threadsafe() == 0 {
 	if C.sqlite3_threadsafe() == 0 {
 		return nil, errors.New("sqlite library was not compiled for thread-safe operation")
 		return nil, errors.New("sqlite library was not compiled for thread-safe operation")
@@ -790,6 +803,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 	busyTimeout := 5000
 	busyTimeout := 5000
 	foreignKeys := -1
 	foreignKeys := -1
 	recursiveTriggers := -1
 	recursiveTriggers := -1
+	mutex := C.int(C.SQLITE_OPEN_FULLMUTEX)
 	pos := strings.IndexRune(dsn, '?')
 	pos := strings.IndexRune(dsn, '?')
 	if pos >= 1 {
 	if pos >= 1 {
 		params, err := url.ParseQuery(dsn[pos+1:])
 		params, err := url.ParseQuery(dsn[pos+1:])
@@ -856,6 +870,18 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 			}
 			}
 		}
 		}
 
 
+		// _mutex
+		if val := params.Get("_mutex"); val != "" {
+			switch val {
+			case "no":
+				mutex = C.SQLITE_OPEN_NOMUTEX
+			case "full":
+				mutex = C.SQLITE_OPEN_FULLMUTEX
+			default:
+				return nil, fmt.Errorf("Invalid _mutex: %v", val)
+			}
+		}
+
 		if !strings.HasPrefix(dsn, "file:") {
 		if !strings.HasPrefix(dsn, "file:") {
 			dsn = dsn[:pos]
 			dsn = dsn[:pos]
 		}
 		}
@@ -865,9 +891,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 	name := C.CString(dsn)
 	name := C.CString(dsn)
 	defer C.free(unsafe.Pointer(name))
 	defer C.free(unsafe.Pointer(name))
 	rv := C._sqlite3_open_v2(name, &db,
 	rv := C._sqlite3_open_v2(name, &db,
-		C.SQLITE_OPEN_FULLMUTEX|
-			C.SQLITE_OPEN_READWRITE|
-			C.SQLITE_OPEN_CREATE,
+		mutex|C.SQLITE_OPEN_READWRITE|C.SQLITE_OPEN_CREATE,
 		nil)
 		nil)
 	if rv != 0 {
 	if rv != 0 {
 		return nil, Error{Code: ErrNo(rv)}
 		return nil, Error{Code: ErrNo(rv)}
@@ -1070,7 +1094,7 @@ func (s *SQLiteStmt) bind(args []namedValue) error {
 		case int64:
 		case int64:
 			rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
 			rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
 		case bool:
 		case bool:
-			if bool(v) {
+			if v {
 				rv = C.sqlite3_bind_int(s.s, n, 1)
 				rv = C.sqlite3_bind_int(s.s, n, 1)
 			} else {
 			} else {
 				rv = C.sqlite3_bind_int(s.s, n, 0)
 				rv = C.sqlite3_bind_int(s.s, n, 0)
@@ -1121,18 +1145,20 @@ func (s *SQLiteStmt) query(ctx context.Context, args []namedValue) (driver.Rows,
 		done:     make(chan struct{}),
 		done:     make(chan struct{}),
 	}
 	}
 
 
-	go func(db *C.sqlite3) {
-		select {
-		case <-ctx.Done():
+	if ctxdone := ctx.Done(); ctxdone != nil {
+		go func(db *C.sqlite3) {
 			select {
 			select {
+			case <-ctxdone:
+				select {
+				case <-rows.done:
+				default:
+					C.sqlite3_interrupt(db)
+					rows.Close()
+				}
 			case <-rows.done:
 			case <-rows.done:
-			default:
-				C.sqlite3_interrupt(db)
-				rows.Close()
 			}
 			}
-		case <-rows.done:
-		}
-	}(s.c.db)
+		}(s.c.db)
+	}
 
 
 	return rows, nil
 	return rows, nil
 }
 }
@@ -1166,19 +1192,21 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
 		return nil, err
 		return nil, err
 	}
 	}
 
 
-	done := make(chan struct{})
-	defer close(done)
-	go func(db *C.sqlite3) {
-		select {
-		case <-done:
-		case <-ctx.Done():
+	if ctxdone := ctx.Done(); ctxdone != nil {
+		done := make(chan struct{})
+		defer close(done)
+		go func(db *C.sqlite3) {
 			select {
 			select {
 			case <-done:
 			case <-done:
-			default:
-				C.sqlite3_interrupt(db)
+			case <-ctxdone:
+				select {
+				case <-done:
+				default:
+					C.sqlite3_interrupt(db)
+				}
 			}
 			}
-		}
-	}(s.c.db)
+		}(s.c.db)
+	}
 
 
 	var rowid, changes C.longlong
 	var rowid, changes C.longlong
 	rv := C._sqlite3_step(s.s, &rowid, &changes)
 	rv := C._sqlite3_step(s.s, &rowid, &changes)
@@ -1272,7 +1300,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
 		case C.SQLITE_INTEGER:
 		case C.SQLITE_INTEGER:
 			val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
 			val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
 			switch rc.decltype[i] {
 			switch rc.decltype[i] {
-			case "timestamp", "datetime", "date":
+			case columnTimestamp, columnDatetime, columnDate:
 				var t time.Time
 				var t time.Time
 				// Assume a millisecond unix timestamp if it's 13 digits -- too
 				// Assume a millisecond unix timestamp if it's 13 digits -- too
 				// large to be a reasonable timestamp in seconds.
 				// large to be a reasonable timestamp in seconds.
@@ -1303,10 +1331,10 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
 			n := int(C.sqlite3_column_bytes(rc.s.s, C.int(i)))
 			n := int(C.sqlite3_column_bytes(rc.s.s, C.int(i)))
 			switch dest[i].(type) {
 			switch dest[i].(type) {
 			case sql.RawBytes:
 			case sql.RawBytes:
-				dest[i] = (*[1 << 30]byte)(unsafe.Pointer(p))[0:n]
+				dest[i] = (*[1 << 30]byte)(p)[0:n]
 			default:
 			default:
 				slice := make([]byte, n)
 				slice := make([]byte, n)
-				copy(slice[:], (*[1 << 30]byte)(unsafe.Pointer(p))[0:n])
+				copy(slice[:], (*[1 << 30]byte)(p)[0:n])
 				dest[i] = slice
 				dest[i] = slice
 			}
 			}
 		case C.SQLITE_NULL:
 		case C.SQLITE_NULL:
@@ -1319,7 +1347,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
 			s := C.GoStringN((*C.char)(unsafe.Pointer(C.sqlite3_column_text(rc.s.s, C.int(i)))), C.int(n))
 			s := C.GoStringN((*C.char)(unsafe.Pointer(C.sqlite3_column_text(rc.s.s, C.int(i)))), C.int(n))
 
 
 			switch rc.decltype[i] {
 			switch rc.decltype[i] {
-			case "timestamp", "datetime", "date":
+			case columnTimestamp, columnDatetime, columnDate:
 				var t time.Time
 				var t time.Time
 				s = strings.TrimSuffix(s, "Z")
 				s = strings.TrimSuffix(s, "Z")
 				for _, format := range SQLiteTimestampFormats {
 				for _, format := range SQLiteTimestampFormats {

+ 2 - 0
vendor/github.com/mattn/go-sqlite3/sqlite3_go18.go

@@ -1,3 +1,5 @@
+// +build cgo
+
 // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
 // Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
 //
 //
 // Use of this source code is governed by an MIT-style
 // Use of this source code is governed by an MIT-style

+ 12 - 0
vendor/github.com/mattn/go-sqlite3/sqlite3_solaris.go

@@ -0,0 +1,12 @@
+// Copyright (C) 2018 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
+//
+// Use of this source code is governed by an MIT-style
+// license that can be found in the LICENSE file.
+// +build solaris
+
+package sqlite3
+
+/*
+#cgo CFLAGS: -D__EXTENSIONS__=1
+*/
+import "C"

+ 12 - 10
vendor/github.com/mattn/go-sqlite3/sqlite3_trace.go

@@ -28,10 +28,10 @@ import (
 // Trace... constants identify the possible events causing callback invocation.
 // Trace... constants identify the possible events causing callback invocation.
 // Values are same as the corresponding SQLite Trace Event Codes.
 // Values are same as the corresponding SQLite Trace Event Codes.
 const (
 const (
-	TraceStmt    = C.SQLITE_TRACE_STMT
-	TraceProfile = C.SQLITE_TRACE_PROFILE
-	TraceRow     = C.SQLITE_TRACE_ROW
-	TraceClose   = C.SQLITE_TRACE_CLOSE
+	TraceStmt    = uint32(C.SQLITE_TRACE_STMT)
+	TraceProfile = uint32(C.SQLITE_TRACE_PROFILE)
+	TraceRow     = uint32(C.SQLITE_TRACE_ROW)
+	TraceClose   = uint32(C.SQLITE_TRACE_CLOSE)
 )
 )
 
 
 type TraceInfo struct {
 type TraceInfo struct {
@@ -71,7 +71,7 @@ type TraceUserCallback func(TraceInfo) int
 
 
 type TraceConfig struct {
 type TraceConfig struct {
 	Callback        TraceUserCallback
 	Callback        TraceUserCallback
-	EventMask       C.uint
+	EventMask       uint32
 	WantExpandedSQL bool
 	WantExpandedSQL bool
 }
 }
 
 
@@ -105,6 +105,8 @@ func traceCallbackTrampoline(
 	// Parameter named 'X' in SQLite docs (eXtra event data?):
 	// Parameter named 'X' in SQLite docs (eXtra event data?):
 	xValue unsafe.Pointer) C.int {
 	xValue unsafe.Pointer) C.int {
 
 
+	eventCode := uint32(traceEventCode)
+
 	if ctx == nil {
 	if ctx == nil {
 		panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
 		panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
 	}
 	}
@@ -114,7 +116,7 @@ func traceCallbackTrampoline(
 
 
 	var traceConf TraceConfig
 	var traceConf TraceConfig
 	var found bool
 	var found bool
-	if traceEventCode == TraceClose {
+	if eventCode == TraceClose {
 		// clean up traceMap: 'pop' means get and delete
 		// clean up traceMap: 'pop' means get and delete
 		traceConf, found = popTraceMapping(connHandle)
 		traceConf, found = popTraceMapping(connHandle)
 	} else {
 	} else {
@@ -123,16 +125,16 @@ func traceCallbackTrampoline(
 
 
 	if !found {
 	if !found {
 		panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
 		panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
-			connHandle, traceEventCode))
+			connHandle, eventCode))
 	}
 	}
 
 
 	var info TraceInfo
 	var info TraceInfo
 
 
-	info.EventCode = uint32(traceEventCode)
+	info.EventCode = eventCode
 	info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
 	info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
 	info.ConnHandle = connHandle
 	info.ConnHandle = connHandle
 
 
-	switch traceEventCode {
+	switch eventCode {
 	case TraceStmt:
 	case TraceStmt:
 		info.StmtHandle = uintptr(p)
 		info.StmtHandle = uintptr(p)
 
 
@@ -183,7 +185,7 @@ func traceCallbackTrampoline(
 	// registering this callback trampoline with SQLite --- for cleanup.
 	// registering this callback trampoline with SQLite --- for cleanup.
 	// In the future there may be more events forced to "selected" in SQLite
 	// In the future there may be more events forced to "selected" in SQLite
 	// for the driver's needs.
 	// for the driver's needs.
-	if traceConf.EventMask&traceEventCode == 0 {
+	if traceConf.EventMask&eventCode == 0 {
 		return 0
 		return 0
 	}
 	}
 
 

+ 7 - 0
vendor/github.com/mattn/go-sqlite3/sqlite3ext.h

@@ -293,6 +293,9 @@ struct sqlite3_api_routines {
   int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
   int (*bind_pointer)(sqlite3_stmt*,int,void*,const char*,void(*)(void*));
   void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
   void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
   void *(*value_pointer)(sqlite3_value*,const char*);
   void *(*value_pointer)(sqlite3_value*,const char*);
+  int (*vtab_nochange)(sqlite3_context*);
+  int (*value_nochange)(sqlite3_value*);
+  const char *(*vtab_collation)(sqlite3_index_info*,int);
 };
 };
 
 
 /*
 /*
@@ -559,6 +562,10 @@ typedef int (*sqlite3_loadext_entry)(
 #define sqlite3_bind_pointer           sqlite3_api->bind_pointer
 #define sqlite3_bind_pointer           sqlite3_api->bind_pointer
 #define sqlite3_result_pointer         sqlite3_api->result_pointer
 #define sqlite3_result_pointer         sqlite3_api->result_pointer
 #define sqlite3_value_pointer          sqlite3_api->value_pointer
 #define sqlite3_value_pointer          sqlite3_api->value_pointer
+/* Version 3.22.0 and later */
+#define sqlite3_vtab_nochange          sqlite3_api->vtab_nochange
+#define sqlite3_value_nochange         sqlite3_api->value_nochange
+#define sqlite3_vtab_collation         sqlite3_api->vtab_collation
 #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
 #endif /* !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION) */
 
 
 #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)
 #if !defined(SQLITE_CORE) && !defined(SQLITE_OMIT_LOAD_EXTENSION)

+ 21 - 0
vendor/github.com/mattn/go-sqlite3/static_mock.go

@@ -0,0 +1,21 @@
+// +build !cgo
+
+package sqlite3
+
+import (
+	"database/sql"
+	"database/sql/driver"
+	"errors"
+)
+
+func init() {
+	sql.Register("sqlite3", &SQLiteDriverMock{})
+}
+
+type SQLiteDriverMock struct{}
+
+var errorMsg = errors.New("Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work. This is a stub")
+
+func (SQLiteDriverMock) Open(s string) (driver.Conn, error) {
+	return nil, errorMsg
+}

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů