Просмотр исходного кода

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 7 лет назад
Родитель
Сommit
6cd83e182a
82 измененных файлов с 2295 добавлено и 1127 удалено
  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'
       - run:
           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:
           name: Trigger Windows build
           command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} master'
       - run:
           name: Trigger Docker build
-          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN}'
+          command: './scripts/trigger_docker_build.sh ${TRIGGER_GRAFANA_PACKER_CIRCLECI_TOKEN} master-$(echo "${CIRCLE_SHA1}" | cut -b1-7)'
       - run:
           name: Publish to Grafana.com
-          command: './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:
     docker:
@@ -241,8 +246,8 @@ workflows:
             - mysql-integration-test
             - postgres-integration-test
           filters:
-            branches:
-              only: master
+           branches:
+             only: master
   release:
     jobs:
       - build-all:

+ 17 - 0
CHANGELOG.md

@@ -2,6 +2,21 @@
 
 ### 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)
 * **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)
@@ -35,6 +50,8 @@
 * **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**: 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)
 

+ 4 - 4
Gopkg.lock

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

+ 2 - 2
Gopkg.toml

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

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

@@ -20,7 +20,7 @@ queries through the use of query references.
 ## Adding the data source
 
 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.
 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.
-More information in the [Provisioning documentation](/features/datasources/prometheus/).
+More information in the [Provisioning documentation](/administration/provisioning/).
 
 <div class="clearfix"></div>
 

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

@@ -331,6 +331,27 @@ Content-Type: application/json
 {"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 /api/orgs/:orgId/users`

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

@@ -419,25 +419,33 @@ allowed_organizations = github 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
 [auth.google]
 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
 auth_url = https://accounts.google.com/o/oauth2/auth
 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
 ```
 
+## 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
 
 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)
 
+> 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.
 
 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)
 - QueryCtrl (Required)
 - ConfigCtrl (Required)
-- QueryOptionsCtrl
 - AnnotationsQueryCtrl
 
 ## 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.
 
-## 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
 
 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
 
-- [Go 1.9.2](https://golang.org/dl/)
+- [Go 1.10](https://golang.org/dl/)
 - [Git](https://git-scm.com/downloads)
 - [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.
@@ -66,13 +66,13 @@ You can run a local instance of Grafana by running:
 ```bash
 ./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`
 
 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
 does this.
@@ -124,7 +124,7 @@ Learn more about Grafana config options in the [Configuration section](/installa
 ## 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.
 
-## Troubleshooting
+# Troubleshooting
 
 **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"`
 	Dashboard *simplejson.Json               `json:"dashboard"`
 	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,
 		"ldapEnabled":             setting.LdapEnabled,
 		"alertingEnabled":         setting.AlertingEnabled,
+		"exploreEnabled":          setting.ExploreEnabled,
 		"googleAnalyticsId":       setting.GoogleAnalyticsId,
 		"disableLoginForm":        setting.DisableLoginForm,
 		"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 {
 			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{
 			Text:     "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{
 			Id:       "cfg",
 			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{
 				Divider: true, HideFromTabs: true, Id: "admin-divider", Text: "Text",
 			})
+		}
+
+		if c.IsGrafanaAdmin {
 			cfgNode.Children = append(cfgNode.Children, &dtos.NavLink{
 				Text:         "Server Admin",
 				HideFromTabs: true,

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

@@ -25,12 +25,9 @@ import (
 )
 
 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 {
@@ -48,6 +45,10 @@ type DataSourceProxy struct {
 	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 {
 	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() {
 	if err := proxy.validateRequest(); err != 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) {
-	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)) {
 			logger.Info("Using token from cache")
 			return cachedToken.AccessToken, nil
@@ -350,12 +358,16 @@ func (proxy *DataSourceProxy) getAccessToken(data templateData) (string, error)
 
 	expiresOnEpoch, _ := strconv.ParseInt(token.ExpiresOnString, 10, 64)
 	token.ExpiresOn = time.Unix(expiresOnEpoch, 0)
-	tokenCache[proxy.ds.Id] = &token
+	tokenCache[proxy.getAccessTokenCacheKey()] = &token
 
 	logger.Info("Got new access token", "ExpiresOn", token.ExpiresOn)
 	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) {
 	t, err := template.New("content").Parse(text)
 	if err != nil {

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

@@ -1,9 +1,13 @@
 package pluginproxy
 
 import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
 	"net/http"
 	"net/url"
 	"testing"
+	"time"
 
 	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() {
 			plugin := &plugins.DataSourcePlugin{}
 			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,
 		Inputs:    apiCmd.Inputs,
 		Overwrite: apiCmd.Overwrite,
+		FolderId:  apiCmd.FolderId,
 		Dashboard: apiCmd.Dashboard,
 	}
 

+ 3 - 1
pkg/plugins/dashboard_importer.go

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

+ 1 - 0
pkg/plugins/dashboards.go

@@ -17,6 +17,7 @@ type PluginDashboardInfoDTO struct {
 	ImportedUrl      string `json:"importedUrl"`
 	Slug             string `json:"slug"`
 	DashboardId      int64  `json:"dashboardId"`
+	FolderId         int64  `json:"folderId"`
 	ImportedRevision int64  `json:"importedRevision"`
 	Revision         int64  `json:"revision"`
 	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 {
 	NotifierBase
-	Url       string
-	Recipient string
-	Mention   string
-	log       log.Logger
+	Url string
+	log log.Logger
 }
 
 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{}{
-		"@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(),
 		"themeColor": evalContext.GetStateModel().Color,
 		"sections": []map[string]interface{}{

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

@@ -89,7 +89,7 @@ func (ss *SqlStore) ensureAdminUser() error {
 	systemUserCountQuery := m.GetSystemUserCountStatsQuery{}
 
 	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 {

+ 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) {
 	version, err := ds.JsonData.Get("esVersion").Int()
 	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()
 	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()

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

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

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

@@ -4,85 +4,39 @@ import (
 	"testing"
 
 	"github.com/grafana/grafana/pkg/tsdb"
-	. "github.com/smartystreets/goconvey/convey"
 )
 
 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.keydown(userActivityDetected);
       // 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
       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>
       Folder
     </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 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;
   }
+
+  importDashboardUrl() {
+    let url = 'dashboard/import';
+
+    if (this.folderId) {
+      url += `?folderId=${this.folderId}`;
+    }
+
+    return url;
+  }
 }
 
 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">
           <i class="gicon gicon-folder-new"></i> New folder
         </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
         </a>
         <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>
       </div>
     </div>

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

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

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

@@ -1,6 +1,7 @@
 import $ from 'jquery';
 import _ from 'lodash';
 
+import config from 'app/core/config';
 import coreModule from 'app/core/core_module';
 import appEvents from 'app/core/app_events';
 import { encodePathComponent } from 'app/core/utils/location_util';
@@ -178,7 +179,7 @@ export class KeybindingSrv {
     });
 
     // jump to explore if permissions allow
-    if (this.contextSrv.isEditor) {
+    if (this.contextSrv.isEditor && config.exploreEnabled) {
       this.bind('x', async () => {
         if (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;
     }
   }
+
+  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.mwatt = kbn.formatBuilders.decimalSIPrefix('W', -1);
 kbn.valueFormats.kwattm = kbn.formatBuilders.decimalSIPrefix('W/Min', 1);
+kbn.valueFormats.Wm2 = kbn.formatBuilders.fixedUnit('W/m2');
 kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
 kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
 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.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
 kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
+kbn.valueFormats.pressurekpa = kbn.formatBuilders.fixedUnit('kPa');
 kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
 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.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
 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
 kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°');
@@ -1014,6 +1019,7 @@ kbn.getUnitFormats = function() {
         { text: 'Watt (W)', value: 'watt' },
         { text: 'Kilowatt (kW)', value: 'kwatt' },
         { text: 'Milliwatt (mW)', value: 'mwatt' },
+        { text: 'Watt per square metre (W/m2)', value: 'Wm2' },
         { text: 'Volt-ampere (VA)', value: 'voltamp' },
         { text: 'Kilovolt-ampere (kVA)', value: 'kvoltamp' },
         { text: 'Volt-ampere reactive (var)', value: 'voltampreact' },
@@ -1049,6 +1055,7 @@ kbn.getUnitFormats = function() {
         { text: 'Bars', value: 'pressurebar' },
         { text: 'Kilobars', value: 'pressurekbar' },
         { text: 'Hectopascals', value: 'pressurehpa' },
+        { text: 'Kilopascals', value: 'pressurekpa' },
         { text: 'Inches of mercury', value: 'pressurehg' },
         { text: 'PSI', value: 'pressurepsi' },
       ],
@@ -1069,6 +1076,9 @@ kbn.getUnitFormats = function() {
         { text: 'Cubic meters/sec (cms)', value: 'flowcms' },
         { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
         { 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);
-      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);
-      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);
-      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;
   autoGenerateUid: boolean;
   autoGenerateUidValue: string;
+  folderId: number;
+  initialFolderTitle: string;
+  isValidFolderSelection: boolean;
 
   /** @ngInject */
   constructor(private backendSrv, private validationSrv, navModelSrv, private $location, $routeParams) {
@@ -31,6 +34,8 @@ export class DashboardImportCtrl {
     this.uidExists = false;
     this.autoGenerateUid = true;
     this.autoGenerateUidValue = 'auto-generated';
+    this.folderId = $routeParams.folderId ? Number($routeParams.folderId) || 0 : null;
+    this.initialFolderTitle = 'Select a folder';
 
     // check gnetId in url
     if ($routeParams.gnetId) {
@@ -102,8 +107,9 @@ export class DashboardImportCtrl {
     this.nameExists = false;
 
     this.validationSrv
-      .validateNewDashboardName(0, this.dash.title)
+      .validateNewDashboardName(this.folderId, this.dash.title)
       .then(() => {
+        this.nameExists = false;
         this.hasNameValidationError = false;
       })
       .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() {
     var inputs = this.inputs.map(input => {
       return {
@@ -153,6 +176,7 @@ export class DashboardImportCtrl {
         dashboard: this.dash,
         overwrite: true,
         inputs: inputs,
+        folderId: this.folderId,
       })
       .then(res => {
         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) {
     let regex = new RegExp(filter, 'i');
     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">
                 <input
                   type="text"
-                  className="gf-form-input max-width-20"
+                  autoFocus
+                  className="gf-form-input gf-form--grow"
                   placeholder="Panel Search Filter"
                   value={this.state.filter}
                   onChange={this.filterChange.bind(this)}
+                  onKeyPress={this.filterKeyPress.bind(this)}
                 />
                 <i className="gf-form-input-icon fa fa-search" />
               </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,
     });
 
-    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 (
       <div className={classes}>
         <a className="dashboard-row__title pointer" onClick={this.toggle}>
           <i className={chevronClass} />
           {title}
-          <span className="dashboard-row__panel_count">({hiddenPanels} hidden panels)</span>
+          <span className="dashboard-row__panel_count">
+            ({count} {panels})
+          </span>
         </a>
         {this.dashboard.meta.canEdit === true && (
           <div className="dashboard-row__actions">
@@ -104,6 +107,11 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
             </a>
           </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>
     );

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

@@ -132,23 +132,26 @@ export class FolderPickerCtrl {
   }
 
   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();
-    }
+    });
   }
 
   private onFolderLoad() {

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

@@ -80,6 +80,20 @@
         </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 gf-form--grow">
           <span class="gf-form-label width-15">
@@ -132,10 +146,10 @@
     </div>
 
     <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
       </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)
       </button>
       <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 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>
   </form>
 </div>
@@ -68,6 +77,7 @@ export class SaveDashboardModalCtrl {
   originalCurrent = [];
   max: number;
   saveForm: any;
+  isSaving: boolean;
   dismiss: () => void;
   timeChange = false;
   variableValueChange = false;
@@ -76,6 +86,7 @@ export class SaveDashboardModalCtrl {
   constructor(private dashboardSrv) {
     this.message = '';
     this.max = 64;
+    this.isSaving = false;
     this.templating = dashboardSrv.dash.templating.list;
 
     this.compareTemplating();
@@ -126,6 +137,8 @@ export class SaveDashboardModalCtrl {
     var dashboard = this.dashboardSrv.getCurrent();
     var saveModel = dashboard.getSaveModelClone(options);
 
+    this.isSaving = true;
+
     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() {
     const items = [];
-    if (this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
+    if (config.exploreEnabled && this.contextSrv.isEditor && this.datasource && this.datasource.supportsExplore) {
       items.push({
         text: '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>
     </ul>
   </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>`;
 
 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/config', () => {
+  return {
+    exploreEnabled: true,
+    panels: {
+      test: {
+        id: 'test',
+        name: 'test',
+      },
+    },
+  };
+});
 
-import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
 import q from 'q';
 import { PanelModel } from 'app/features/dashboard/panel_model';
+import { MetricsPanelCtrl } from '../metrics_panel_ctrl';
 
 describe('MetricsPanelCtrl', () => {
   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 { describe, beforeEach, it, expect } from 'test/lib/common';
 import { PlaylistEditCtrl } from '../playlist_edit_ctrl';
 
 describe('PlaylistEditCtrl', () => {
@@ -20,13 +19,13 @@ describe('PlaylistEditCtrl', () => {
 
   describe('searchresult returns 2 dashboards, ', () => {
     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', () => {
       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, ', () => {
@@ -37,16 +36,16 @@ describe('PlaylistEditCtrl', () => {
       });
 
       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', () => {
-        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', () => {
-        expect(ctx.dashboardresult.length).to.be(2);
+        expect(ctx.dashboardresult.length).toBe(2);
       });
 
       describe('removes one dashboard from playlist, ', () => {
@@ -57,14 +56,14 @@ describe('PlaylistEditCtrl', () => {
         });
 
         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', () => {
-          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) {
         // do not show access option if direct access is disabled
         $scope.showAccessOption = $scope.noDirectAccess !== 'true';
+        $scope.showAccessHelp = false;
+        $scope.toggleAccessHelp = function() {
+          $scope.showAccessHelp = !$scope.showAccessHelp;
+        };
 
         $scope.getSuggestUrls = function() {
           return [$scope.suggestUrl];

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

@@ -30,7 +30,7 @@
         </div>
       </div>
       <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;
           <i class="fa fa-caret-down" ng-show="ctrl.showAccessHelp"></i>
           <i class="fa fa-caret-right" ng-hide="ctrl.showAccessHelp">&nbsp;</i>
@@ -38,7 +38,7 @@
       </div>
     </div>
 
-    <div class="alert alert-info" ng-show="ctrl.showAccessHelp">
+    <div class="alert alert-info" ng-show="showAccessHelp">
       <div class="alert-body">
         <p>
           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 { UnknownPanelCtrl } from 'app/plugins/panel/unknown/module';
-import { DashboardRowCtrl } from './row_ctrl';
 
 /** @ngInject **/
 function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $templateCache) {
@@ -59,15 +58,6 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
   }
 
   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 = {
       name: 'panel-plugin-' + scope.panel.type,
       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
       case 'annotations-query-ctrl': {
         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 angular from 'angular';
 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 TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
@@ -69,6 +78,14 @@ exposeToPlugin('d3', d3);
 exposeToPlugin('rxjs/Subject', Subject);
 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
 exposeToPlugin('vendor/npm/rxjs/Rx', {
   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 { ElasticConfigCtrl } from './config_ctrl';
 
-class ElasticQueryOptionsCtrl {
-  static templateUrl = 'partials/query.options.html';
-}
-
 class ElasticAnnotationsQueryCtrl {
   static templateUrl = 'partials/annotations.editor.html';
 }
@@ -14,6 +10,5 @@ export {
   ElasticDatasource as Datasource,
   ElasticQueryCtrl as QueryCtrl,
   ElasticConfigCtrl as ConfigCtrl,
-  ElasticQueryOptionsCtrl as QueryOptionsCtrl,
   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';
 
-describe('ElasticResponse', function() {
+describe('ElasticResponse', () => {
   var targets;
   var response;
   var result;
 
-  describe('simple query and count', function() {
-    beforeEach(function() {
+  describe('simple query and count', () => {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -39,19 +38,19 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -85,22 +84,22 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -141,18 +140,18 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -199,20 +198,20 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -246,21 +245,21 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -322,21 +321,21 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -385,19 +384,19 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -420,16 +419,16 @@ describe('ElasticResponse', function() {
       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;
 
-    beforeEach(function() {
+    beforeEach(() => {
       targets = [
         {
           refId: 'A',
@@ -472,16 +471,16 @@ describe('ElasticResponse', function() {
       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 = [
         {
           refId: 'A',
@@ -528,14 +527,14 @@ describe('ElasticResponse', function() {
       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 = [
         {
           refId: 'A',
@@ -570,21 +569,21 @@ describe('ElasticResponse', function() {
       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 = [
         {
           refId: 'A',
@@ -615,15 +614,15 @@ describe('ElasticResponse', function() {
       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 = [
         {
           refId: 'A',
@@ -657,13 +656,13 @@ describe('ElasticResponse', function() {
       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" />
 
-import { describe, it, expect } from 'test/lib/common';
 import moment from 'moment';
 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 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 from = new Date(2015, 4, 30, 1, 2, 3);
         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 from = new Date(1432940523000);
         var to = new Date(1433153106000);
 
         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';
 
-describe('ElasticQueryBuilder', function() {
+describe('ElasticQueryBuilder', () => {
   var builder;
 
-  beforeEach(function() {
+  beforeEach(() => {
     builder = new ElasticQueryBuilder({ timeField: '@timestamp' });
   });
 
-  it('with defaults', function() {
+  it('with defaults', () => {
     var query = builder.build({
       metrics: [{ type: 'Count', id: '0' }],
       timeField: '@timestamp',
       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({
       timeField: '@timestamp',
       esVersion: 5,
@@ -31,11 +30,11 @@ describe('ElasticQueryBuilder', function() {
       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({
       metrics: [{ type: 'count', id: '1' }],
       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(
       {
         metrics: [{ type: 'avg', field: '@value', id: '1' }],
@@ -60,10 +59,10 @@ describe('ElasticQueryBuilder', function() {
     );
 
     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(
       {
         metrics: [{ type: 'count', id: '1' }, { type: 'avg', field: '@value', id: '5' }],
@@ -84,11 +83,11 @@ describe('ElasticQueryBuilder', function() {
     var firstLevel = query.aggs['2'];
     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(
       {
         metrics: [
@@ -109,11 +108,11 @@ describe('ElasticQueryBuilder', function() {
 
     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({
       metrics: [{ type: 'count', id: '1' }],
       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({
       timeField: '@timestamp',
       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({
       metrics: [{ type: 'raw_document', id: '1', settings: {} }],
       timeField: '@timestamp',
       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({
       metrics: [{ type: 'raw_document', id: '1', settings: { size: 1337 } }],
       timeField: '@timestamp',
       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({
       metrics: [
         {
@@ -198,12 +197,12 @@ describe('ElasticQueryBuilder', function() {
 
     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({
       metrics: [
         {
@@ -227,13 +226,13 @@ describe('ElasticQueryBuilder', function() {
 
     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({
       metrics: [
         {
@@ -252,12 +251,12 @@ describe('ElasticQueryBuilder', function() {
 
     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({
       metrics: [{ id: '1', type: 'count' }],
       bucketAggs: [
@@ -271,13 +270,13 @@ describe('ElasticQueryBuilder', function() {
     });
 
     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(
       {
         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';
 
 describe('InfluxQueryBuilder', function() {

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

@@ -23,10 +23,11 @@
     </div>
 
     <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)"
-											label-class="width-6">
+                      label-class="width-6"
+                      initial-title="'All'"
+                      enable-reset="true">
 			</folder-picker>
     </div>
 

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

@@ -17,7 +17,7 @@ class DashListCtrl extends PanelCtrl {
     search: false,
     starred: true,
     headings: true,
-    folderId: 0,
+    folderId: null,
   };
 
   /** @ngInject */
@@ -85,7 +85,8 @@ class DashListCtrl extends PanelCtrl {
       limit: this.panel.limit,
       query: this.panel.query,
       tag: this.panel.tags,
-      folderId: this.panel.folderId,
+      folderIds: this.panel.folderId,
+      type: 'dash-db',
     };
 
     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 GraphTooltip from '../graph_tooltip';
 
 var scope = {
-  appEvent: sinon.spy(),
-  onAppEvent: sinon.spy(),
+  appEvent: jest.fn(),
+  onAppEvent: jest.fn(),
   ctrl: {},
 };
 
@@ -47,22 +47,22 @@ describe('findHoverIndexFromData', function() {
 
   it('should return 0 if posX out of lower bounds', function() {
     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() {
     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() {
     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() {
     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() {
-    expect(ctx.results.length).to.be(2);
+    expect(ctx.results.length).toBe(2);
   });
 
   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() {
-    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() {
-    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() {
-    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() {
-    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 TimeSeries from 'app/core/time_series2';
 import { ThresholdManager } from '../threshold_manager';
@@ -38,16 +36,16 @@ describe('ThresholdManager', function() {
       it('should add fill for threshold with fill: true', function() {
         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() {
         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() {
         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() {
         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() {
         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() {
         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() {
         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() {
         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() {
         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() {
         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';
 
 describe('grafanaSingleStat', function() {
@@ -11,31 +9,31 @@ describe('grafanaSingleStat', function() {
       };
 
       it('5 should return green', () => {
-        expect(getColorForValue(data, 5)).to.be('green');
+        expect(getColorForValue(data, 5)).toBe('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', () => {
-        expect(getColorForValue(data, 20)).to.be('yellow');
+        expect(getColorForValue(data, 20)).toBe('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', () => {
-        expect(getColorForValue(data, 25)).to.be('yellow');
+        expect(getColorForValue(data, 25)).toBe('yellow');
       });
 
       it('50 should return red', () => {
-        expect(getColorForValue(data, 50)).to.be('red');
+        expect(getColorForValue(data, 50)).toBe('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', () => {
-      expect(getColorForValue(data, -30)).to.be('green');
+      expect(getColorForValue(data, -30)).toBe('green');
     });
 
     it('1 should return green', () => {
-      expect(getColorForValue(data, 1)).to.be('yellow');
+      expect(getColorForValue(data, 1)).toBe('yellow');
     });
 
     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', () => {
-      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
 .btn-success {
   @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
 .btn-secondary {

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

@@ -11,11 +11,20 @@
       display: inline-block;
     }
 
-    .dashboard-row__drag,
-    .dashboard-row__actions {
+    .dashboard-row__drag {
       visibility: visible;
       opacity: 1;
     }
+
+    .dashboard-row__actions {
+      visibility: hidden;
+    }
+
+    .dashboard-row__toggle-target {
+      flex: 1;
+      cursor: pointer;
+      margin-right: 15px;
+    }
   }
 
   &:hover {
@@ -43,7 +52,6 @@
   color: $text-muted;
   visibility: hidden;
   opacity: 0;
-  flex-grow: 1;
   transition: 200ms opacity ease-in 200ms;
 
   a {
@@ -69,7 +77,7 @@
   cursor: move;
   width: 1rem;
   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;
   visibility: hidden;
   position: absolute;

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

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

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

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

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

@@ -13,6 +13,13 @@ $login-border: #8daac5;
   justify-content: center;
   background-image: url(../img/heatmap_bg_test.svg);
   background-size: cover;
+  color: #d8d9da;
+  & a {
+    color: #d8d9da !important;
+  }
+  & .btn-primary {
+    @include buttonBackground(#ff6600, #bc3e06);
+  }
 }
 
 input:-webkit-autofill,
@@ -25,8 +32,9 @@ textarea:-webkit-autofill:focus,
 select:-webkit-autofill,
 select:-webkit-autofill:hover,
 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 {
@@ -46,6 +54,8 @@ select:-webkit-autofill:focus {
   border: 1px solid $login-border;
   border-radius: 4px;
   opacity: 0.6;
+  background: $black;
+  color: #fbfbfb;
 
   &:focus {
     border: 1px solid $login-border;
@@ -103,7 +113,7 @@ select:-webkit-autofill:focus {
   }
 
   .icon-gf-grafana_wordmark {
-    color: $link-color;
+    color: darken($white, 11%);
     position: relative;
     font-size: 2rem;
     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';
 
 describe("SemVersion", () => {
@@ -8,10 +6,10 @@ describe("SemVersion", () => {
   describe('parsing', () => {
     it('should parse version properly', () => {
       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},
       ];
       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},
       ];
       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 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() });
+
+var global = <any>window;
+global.$ = global.jQuery = $;

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

@@ -23,7 +23,7 @@ module.exports = merge(common, {
   },
 
   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',

Разница между файлами не показана из-за своего большого размера
+ 531 - 106
vendor/github.com/mattn/go-sqlite3/sqlite3-binding.c


Разница между файлами не показана из-за своего большого размера
+ 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>.
 //
 // Use of this source code is governed by an MIT-style
@@ -7,10 +9,13 @@ package sqlite3
 
 /*
 #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_TRACE_SIZE_LIMIT=15
+#cgo CFLAGS: -DSQLITE_OMIT_DEPRECATED
 #cgo CFLAGS: -DSQLITE_DISABLE_INTRINSIC
+#cgo CFLAGS: -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT
 #cgo CFLAGS: -Wno-deprecated-declarations
 #ifndef USE_LIBSQLITE3
 #include <sqlite3-binding.h>
@@ -170,6 +175,12 @@ var SQLiteTimestampFormats = []string{
 	"2006-01-02",
 }
 
+const (
+	columnDate      string = "date"
+	columnDatetime  string = "datetime"
+	columnTimestamp string = "timestamp"
+)
+
 func init() {
 	sql.Register("sqlite3", &SQLiteDriver{})
 }
@@ -389,7 +400,7 @@ func (c *SQLiteConn) RegisterCommitHook(callback func() int) {
 	if callback == nil {
 		C.sqlite3_commit_hook(c.db, nil, nil)
 	} 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 {
 		C.sqlite3_rollback_hook(c.db, nil, nil)
 	} 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 {
 		C.sqlite3_update_hook(c.db, nil, nil)
 	} 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 {
-	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.
@@ -780,6 +791,8 @@ func errorString(err Error) string {
 //     Enable or disable enforcement of foreign keys.  X can be 1 or 0.
 //   _recursive_triggers=X
 //     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) {
 	if C.sqlite3_threadsafe() == 0 {
 		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
 	foreignKeys := -1
 	recursiveTriggers := -1
+	mutex := C.int(C.SQLITE_OPEN_FULLMUTEX)
 	pos := strings.IndexRune(dsn, '?')
 	if 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:") {
 			dsn = dsn[:pos]
 		}
@@ -865,9 +891,7 @@ func (d *SQLiteDriver) Open(dsn string) (driver.Conn, error) {
 	name := C.CString(dsn)
 	defer C.free(unsafe.Pointer(name))
 	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)
 	if rv != 0 {
 		return nil, Error{Code: ErrNo(rv)}
@@ -1070,7 +1094,7 @@ func (s *SQLiteStmt) bind(args []namedValue) error {
 		case int64:
 			rv = C.sqlite3_bind_int64(s.s, n, C.sqlite3_int64(v))
 		case bool:
-			if bool(v) {
+			if v {
 				rv = C.sqlite3_bind_int(s.s, n, 1)
 			} else {
 				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{}),
 	}
 
-	go func(db *C.sqlite3) {
-		select {
-		case <-ctx.Done():
+	if ctxdone := ctx.Done(); ctxdone != nil {
+		go func(db *C.sqlite3) {
 			select {
+			case <-ctxdone:
+				select {
+				case <-rows.done:
+				default:
+					C.sqlite3_interrupt(db)
+					rows.Close()
+				}
 			case <-rows.done:
-			default:
-				C.sqlite3_interrupt(db)
-				rows.Close()
 			}
-		case <-rows.done:
-		}
-	}(s.c.db)
+		}(s.c.db)
+	}
 
 	return rows, nil
 }
@@ -1166,19 +1192,21 @@ func (s *SQLiteStmt) exec(ctx context.Context, args []namedValue) (driver.Result
 		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 {
 			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
 	rv := C._sqlite3_step(s.s, &rowid, &changes)
@@ -1272,7 +1300,7 @@ func (rc *SQLiteRows) Next(dest []driver.Value) error {
 		case C.SQLITE_INTEGER:
 			val := int64(C.sqlite3_column_int64(rc.s.s, C.int(i)))
 			switch rc.decltype[i] {
-			case "timestamp", "datetime", "date":
+			case columnTimestamp, columnDatetime, columnDate:
 				var t time.Time
 				// Assume a millisecond unix timestamp if it's 13 digits -- too
 				// 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)))
 			switch dest[i].(type) {
 			case sql.RawBytes:
-				dest[i] = (*[1 << 30]byte)(unsafe.Pointer(p))[0:n]
+				dest[i] = (*[1 << 30]byte)(p)[0:n]
 			default:
 				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
 			}
 		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))
 
 			switch rc.decltype[i] {
-			case "timestamp", "datetime", "date":
+			case columnTimestamp, columnDatetime, columnDate:
 				var t time.Time
 				s = strings.TrimSuffix(s, "Z")
 				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>.
 //
 // 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.
 // Values are same as the corresponding SQLite Trace Event Codes.
 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 {
@@ -71,7 +71,7 @@ type TraceUserCallback func(TraceInfo) int
 
 type TraceConfig struct {
 	Callback        TraceUserCallback
-	EventMask       C.uint
+	EventMask       uint32
 	WantExpandedSQL bool
 }
 
@@ -105,6 +105,8 @@ func traceCallbackTrampoline(
 	// Parameter named 'X' in SQLite docs (eXtra event data?):
 	xValue unsafe.Pointer) C.int {
 
+	eventCode := uint32(traceEventCode)
+
 	if ctx == nil {
 		panic(fmt.Sprintf("No context (ev 0x%x)", traceEventCode))
 	}
@@ -114,7 +116,7 @@ func traceCallbackTrampoline(
 
 	var traceConf TraceConfig
 	var found bool
-	if traceEventCode == TraceClose {
+	if eventCode == TraceClose {
 		// clean up traceMap: 'pop' means get and delete
 		traceConf, found = popTraceMapping(connHandle)
 	} else {
@@ -123,16 +125,16 @@ func traceCallbackTrampoline(
 
 	if !found {
 		panic(fmt.Sprintf("Mapping not found for handle 0x%x (ev 0x%x)",
-			connHandle, traceEventCode))
+			connHandle, eventCode))
 	}
 
 	var info TraceInfo
 
-	info.EventCode = uint32(traceEventCode)
+	info.EventCode = eventCode
 	info.AutoCommit = (int(C.sqlite3_get_autocommit(contextDB)) != 0)
 	info.ConnHandle = connHandle
 
-	switch traceEventCode {
+	switch eventCode {
 	case TraceStmt:
 		info.StmtHandle = uintptr(p)
 
@@ -183,7 +185,7 @@ func traceCallbackTrampoline(
 	// registering this callback trampoline with SQLite --- for cleanup.
 	// In the future there may be more events forced to "selected" in SQLite
 	// for the driver's needs.
-	if traceConf.EventMask&traceEventCode == 0 {
+	if traceConf.EventMask&eventCode == 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*));
   void (*result_pointer)(sqlite3_context*,void*,const char*,void(*)(void*));
   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_result_pointer         sqlite3_api->result_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) */
 
 #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
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов