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

Merge pull request #4 from grafana/master

Update master
Pavel 7 лет назад
Родитель
Сommit
c041bf2488
100 измененных файлов с 2111 добавлено и 932 удалено
  1. 5 5
      .circleci/config.yml
  2. 1 0
      .gitignore
  3. 8 0
      CHANGELOG.md
  4. 1 1
      Dockerfile
  5. 29 275
      Gopkg.lock
  6. 4 4
      Gopkg.toml
  7. 3 3
      README.md
  8. 1 1
      appveyor.yml
  9. 1 0
      conf/defaults.ini
  10. 4 0
      conf/sample.ini
  11. 12 1
      docs/sources/auth/generic-oauth.md
  12. 1 1
      docs/sources/auth/github.md
  13. 1 1
      docs/sources/auth/gitlab.md
  14. 9 7
      package.json
  15. 11 11
      packaging/publish/publish_both.sh
  16. 112 8
      pkg/api/dashboard_snapshot.go
  17. 87 0
      pkg/api/dashboard_snapshot_test.go
  18. 7 5
      pkg/api/frontendsettings.go
  19. 8 0
      pkg/api/plugins.go
  20. 1 0
      pkg/cmd/grafana-server/server.go
  21. 8 0
      pkg/infra/serverlock/model.go
  22. 116 0
      pkg/infra/serverlock/serverlock.go
  23. 40 0
      pkg/infra/serverlock/serverlock_integration_test.go
  24. 55 0
      pkg/infra/serverlock/serverlock_test.go
  25. 1 1
      pkg/login/ldap.go
  26. 1 1
      pkg/login/ldap_test.go
  27. 21 7
      pkg/middleware/auth_proxy.go
  28. 90 0
      pkg/middleware/middleware_test.go
  29. 13 9
      pkg/models/dashboard_snapshot.go
  30. 1 12
      pkg/plugins/datasource_plugin.go
  31. 4 2
      pkg/services/alerting/notifier.go
  32. 1 1
      pkg/services/alerting/test_notification.go
  33. 8 3
      pkg/services/cleanup/cleanup.go
  34. 12 10
      pkg/services/sqlstore/dashboard_snapshot.go
  35. 2 2
      pkg/services/sqlstore/datasource.go
  36. 4 0
      pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go
  37. 1 0
      pkg/services/sqlstore/migrations/migrations.go
  38. 22 0
      pkg/services/sqlstore/migrations/serverlock_migrations.go
  39. 6 1
      pkg/services/sqlstore/user.go
  40. 22 1
      pkg/services/sqlstore/user_test.go
  41. 15 14
      pkg/setting/setting_oauth.go
  42. 22 16
      pkg/social/social.go
  43. 38 0
      public/app/core/components/Animations/FadeIn.tsx
  44. 1 1
      public/app/core/components/Animations/SlideDown.tsx
  45. 1 0
      public/app/core/components/AppNotifications/AppNotificationItem.tsx
  46. 36 0
      public/app/core/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx
  47. 67 0
      public/app/core/components/CopyToClipboard/CopyToClipboard.tsx
  48. 2 2
      public/app/core/components/CustomScrollbar/CustomScrollbar.tsx
  49. 4 4
      public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  50. 43 0
      public/app/core/components/Form/Element.tsx
  51. 53 0
      public/app/core/components/Form/Input.test.tsx
  52. 94 0
      public/app/core/components/Form/Input.tsx
  53. 19 0
      public/app/core/components/Form/Label.tsx
  54. 11 0
      public/app/core/components/Form/__snapshots__/Input.test.tsx.snap
  55. 3 0
      public/app/core/components/Form/index.ts
  56. 51 0
      public/app/core/components/JSONFormatter/JSONFormatter.tsx
  57. 1 0
      public/app/core/components/Label/Label.tsx
  58. 9 9
      public/app/core/components/PermissionList/AddPermission.tsx
  59. 8 7
      public/app/core/components/PermissionList/DisabledPermissionListItem.tsx
  60. 9 7
      public/app/core/components/PermissionList/PermissionListItem.tsx
  61. 0 25
      public/app/core/components/Picker/DescriptionOption.tsx
  62. 0 52
      public/app/core/components/Picker/DescriptionPicker.tsx
  63. 0 18
      public/app/core/components/Picker/NoOptionsMessage.tsx
  64. 0 22
      public/app/core/components/Picker/PickerOption.tsx
  65. 0 49
      public/app/core/components/Picker/SimplePicker.tsx
  66. 0 17
      public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap
  67. 83 0
      public/app/core/components/PluginHelp/PluginHelp.tsx
  68. 35 0
      public/app/core/components/Portal/Portal.tsx
  69. 72 0
      public/app/core/components/Select/DataSourcePicker.tsx
  70. 2 2
      public/app/core/components/Select/IndicatorsContainer.tsx
  71. 20 0
      public/app/core/components/Select/NoOptionsMessage.tsx
  72. 53 0
      public/app/core/components/Select/OptionGroup.tsx
  73. 2 2
      public/app/core/components/Select/PickerOption.test.tsx
  74. 44 0
      public/app/core/components/Select/PickerOption.tsx
  75. 4 2
      public/app/core/components/Select/ResetStyles.tsx
  76. 232 0
      public/app/core/components/Select/Select.tsx
  77. 0 0
      public/app/core/components/Select/TeamPicker.test.tsx
  78. 9 18
      public/app/core/components/Select/TeamPicker.tsx
  79. 51 0
      public/app/core/components/Select/UnitPicker.tsx
  80. 0 0
      public/app/core/components/Select/UserPicker.test.tsx
  81. 16 18
      public/app/core/components/Select/UserPicker.tsx
  82. 21 0
      public/app/core/components/Select/__snapshots__/PickerOption.test.tsx.snap
  83. 0 29
      public/app/core/components/Select/__snapshots__/TeamPicker.test.tsx.snap
  84. 0 29
      public/app/core/components/Select/__snapshots__/UserPicker.test.tsx.snap
  85. 13 15
      public/app/core/components/SharedPreferences/SharedPreferences.tsx
  86. 9 16
      public/app/core/components/Switch/Switch.tsx
  87. 6 5
      public/app/core/components/TagFilter/TagFilter.tsx
  88. 1 1
      public/app/core/components/TagFilter/TagOption.tsx
  89. 18 35
      public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx
  90. 11 26
      public/app/core/components/Tooltip/Popover.tsx
  91. 72 0
      public/app/core/components/Tooltip/Popper.tsx
  92. 9 28
      public/app/core/components/Tooltip/Tooltip.tsx
  93. 2 2
      public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap
  94. 3 3
      public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap
  95. 88 0
      public/app/core/components/Tooltip/withPopper.tsx
  96. 0 58
      public/app/core/components/Tooltip/withTooltip.tsx
  97. 3 3
      public/app/core/components/code_editor/code_editor.ts
  98. 9 22
      public/app/core/components/colorpicker/ColorPicker.tsx
  99. 1 1
      public/app/core/components/json_explorer/helpers.ts
  100. 1 1
      public/app/core/components/json_explorer/json_explorer.ts

+ 5 - 5
.circleci/config.yml

@@ -19,7 +19,7 @@ version: 2
 jobs:
 jobs:
   mysql-integration-test:
   mysql-integration-test:
     docker:
     docker:
-      - image: circleci/golang:1.11
+      - image: circleci/golang:1.11.4
       - image: circleci/mysql:5.6-ram
       - image: circleci/mysql:5.6-ram
         environment:
         environment:
           MYSQL_ROOT_PASSWORD: rootpass
           MYSQL_ROOT_PASSWORD: rootpass
@@ -39,7 +39,7 @@ jobs:
 
 
   postgres-integration-test:
   postgres-integration-test:
     docker:
     docker:
-      - image: circleci/golang:1.11
+      - image: circleci/golang:1.11.4
       - image: circleci/postgres:9.3-ram
       - image: circleci/postgres:9.3-ram
         environment:
         environment:
           POSTGRES_USER: grafanatest
           POSTGRES_USER: grafanatest
@@ -74,7 +74,7 @@ jobs:
 
 
   gometalinter:
   gometalinter:
     docker:
     docker:
-      - image: circleci/golang:1.11
+      - image: circleci/golang:1.11.4
         environment:
         environment:
           # we need CGO because of go-sqlite3
           # we need CGO because of go-sqlite3
           CGO_ENABLED: 1
           CGO_ENABLED: 1
@@ -117,7 +117,7 @@ jobs:
 
 
   test-backend:
   test-backend:
     docker:
     docker:
-      - image: circleci/golang:1.11
+      - image: circleci/golang:1.11.4
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout
@@ -175,7 +175,7 @@ jobs:
 
 
   build:
   build:
     docker:
     docker:
-     - image: grafana/build-container:1.2.1
+     - image: grafana/build-container:1.2.2
     working_directory: /go/src/github.com/grafana/grafana
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     steps:
       - checkout
       - checkout

+ 1 - 0
.gitignore

@@ -76,3 +76,4 @@ debug.test
 /devenv/bulk_alerting_dashboards/*.json
 /devenv/bulk_alerting_dashboards/*.json
 
 
 /scripts/build/release_publisher/release_publisher
 /scripts/build/release_publisher/release_publisher
+*.patch

+ 8 - 0
CHANGELOG.md

@@ -2,6 +2,7 @@
 
 
 ### New Features
 ### New Features
 * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
 * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
+* **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
 
 
 ### Minor
 ### Minor
 
 
@@ -11,6 +12,13 @@
 * **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
 * **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
 * **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
 * **Admin**: Fix prevent removing last grafana admin permissions [#11067](https://github.com/grafana/grafana/issues/11067), thx [@danielbh](https://github.com/danielbh)
 * **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
 * **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
+* **Admin**: When multiple user invitations, all links are the same as the first user who was invited [#14483](https://github.com/grafana/grafana/issues/14483)
+* **LDAP**: Upgrade go-ldap to v3 [#14548](https://github.com/grafana/grafana/issues/14548)
+* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
+* **OAuth**: Support OAuth providers that are not RFC6749 compliant [#14562](https://github.com/grafana/grafana/issues/14562), thx [@tdabasinskas](https://github.com/tdabasinskas)
+
+### Bug fixes
+* **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 
 
 # 5.4.2 (2018-12-13)
 # 5.4.2 (2018-12-13)
 
 

+ 1 - 1
Dockerfile

@@ -1,5 +1,5 @@
 # Golang build container
 # Golang build container
-FROM golang:1.11
+FROM golang:1.11.4
 
 
 WORKDIR $GOPATH/src/github.com/grafana/grafana
 WORKDIR $GOPATH/src/github.com/grafana/grafana
 
 

+ 29 - 275
Gopkg.lock

@@ -2,39 +2,30 @@
 
 
 
 
 [[projects]]
 [[projects]]
-  digest = "1:f8ad8a53fa865a70efbe215b0ca34735523f50ea39e0efde319ab6fc80089b44"
   name = "cloud.google.com/go"
   name = "cloud.google.com/go"
   packages = ["compute/metadata"]
   packages = ["compute/metadata"]
-  pruneopts = "NUT"
   revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811"
   revision = "056a55f54a6cc77b440b31a56a5e7c3982d32811"
   version = "v0.22.0"
   version = "v0.22.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:167b6f65a6656de568092189ae791253939f076df60231fdd64588ac703892a1"
   name = "github.com/BurntSushi/toml"
   name = "github.com/BurntSushi/toml"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "b26d9c308763d68093482582cea63d69be07a0f0"
   revision = "b26d9c308763d68093482582cea63d69be07a0f0"
   version = "v0.3.0"
   version = "v0.3.0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:7d23e6e1889b8bb4bbb37a564708fdab4497ce232c3a99d66406c975b642a6ff"
   name = "github.com/Unknwon/com"
   name = "github.com/Unknwon/com"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
   revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:1610787cd9726e29d8fecc2a80e43e4fced008a1f560fec6688fc4d946f17835"
   name = "github.com/VividCortex/mysqlerr"
   name = "github.com/VividCortex/mysqlerr"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
   revision = "6c6b55f8796f578c870b7e19bafb16103bc40095"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:58294d68772aab5a8941b7d5d228eff7cccf63f895e914bc9bc38fda80471ea5"
   name = "github.com/aws/aws-sdk-go"
   name = "github.com/aws/aws-sdk-go"
   packages = [
   packages = [
     "aws",
     "aws",
@@ -70,523 +61,399 @@
     "service/ec2",
     "service/ec2",
     "service/ec2/ec2iface",
     "service/ec2/ec2iface",
     "service/s3",
     "service/s3",
-    "service/sts",
+    "service/sts"
   ]
   ]
-  pruneopts = "NUT"
   revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
   revision = "fde4ded7becdeae4d26bf1212916aabba79349b4"
   version = "v1.14.12"
   version = "v1.14.12"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:79cad073c7be02632d3fa52f62486848b089f560db1e94536de83a408c0f4726"
   name = "github.com/benbjohnson/clock"
   name = "github.com/benbjohnson/clock"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19"
   revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:707ebe952a8b3d00b343c01536c79c73771d100f63ec6babeaed5c79e2b8a8dd"
   name = "github.com/beorn7/perks"
   name = "github.com/beorn7/perks"
   packages = ["quantile"]
   packages = ["quantile"]
-  pruneopts = "NUT"
   revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
   revision = "3a771d992973f24aa725d07868b467d1ddfceafb"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:433a2ff0ef4e2f8634614aab3174783c5ff80120b487712db96cc3712f409583"
   name = "github.com/bmizerany/assert"
   name = "github.com/bmizerany/assert"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28"
   revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:d8f9145c361920507a4f85ffb7f70b96beaedacba2ce8c00aa663adb08689d3e"
   name = "github.com/bradfitz/gomemcache"
   name = "github.com/bradfitz/gomemcache"
   packages = ["memcache"]
   packages = ["memcache"]
-  pruneopts = "NUT"
   revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
   revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:8ecb89af7dfe3ac401bdb0c9390b134ef96a97e85f732d2b0604fb7b3977839f"
   name = "github.com/codahale/hdrhistogram"
   name = "github.com/codahale/hdrhistogram"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "3a0bb77429bd3a61596f5e8a3172445844342120"
   revision = "3a0bb77429bd3a61596f5e8a3172445844342120"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:5dba68a1600a235630e208cb7196b24e58fcbb77bb7a6bec08fcd23f081b0a58"
   name = "github.com/codegangsta/cli"
   name = "github.com/codegangsta/cli"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
   revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
   version = "v1.20.0"
   version = "v1.20.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:a2c1d0e43bd3baaa071d1b9ed72c27d78169b2b269f71c105ac4ba34b1be4a39"
   name = "github.com/davecgh/go-spew"
   name = "github.com/davecgh/go-spew"
   packages = ["spew"]
   packages = ["spew"]
-  pruneopts = "NUT"
   revision = "346938d642f2ec3594ed81d874461961cd0faa76"
   revision = "346938d642f2ec3594ed81d874461961cd0faa76"
   version = "v1.1.0"
   version = "v1.1.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:1b318d2dd6cea8a1a8d8ec70348852303bd3e491df74e8bca6e32eb5a4d06970"
   name = "github.com/denisenkom/go-mssqldb"
   name = "github.com/denisenkom/go-mssqldb"
   packages = [
   packages = [
     ".",
     ".",
-    "internal/cp",
+    "internal/cp"
   ]
   ]
-  pruneopts = "NUT"
   revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
   revision = "270bc3860bb94dd3a3ffd047377d746c5e276726"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:2da5f11ad66ff01a27a5c3dba4620b7eee2327be75b32c9ee9f87c9a8001ecbf"
   name = "github.com/facebookgo/inject"
   name = "github.com/facebookgo/inject"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
   revision = "cc1aa653e50f6a9893bcaef89e673e5b24e1e97b"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:1108df7f658c90db041e0d6174d55be689aaeb0585913b9c3c7aab51a3a6b2b1"
   name = "github.com/facebookgo/structtag"
   name = "github.com/facebookgo/structtag"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
   revision = "217e25fb96916cc60332e399c9aa63f5c422ceed"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:ade392a843b2035effb4b4a2efa2c3bab3eb29b992e98bacf9c898b0ecb54e45"
   name = "github.com/fatih/color"
   name = "github.com/fatih/color"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
   revision = "5b77d2a35fb0ede96d138fc9a99f5c9b6aef11b4"
   version = "v1.7.0"
   version = "v1.7.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474"
   name = "github.com/go-ini/ini"
   name = "github.com/go-ini/ini"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
   revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
   version = "v1.36.0"
   version = "v1.36.0"
 
 
-[[projects]]
-  digest = "1:7e1c00b9959544fa1ccca7cf0407a5b29ac6d5201059c4fac6f599cb99bfd24d"
-  name = "github.com/go-ldap/ldap"
-  packages = ["."]
-  pruneopts = "NUT"
-  revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9"
-  version = "v2.5.1"
-
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:682a0aca743a1a4a36697f3d7f86c0ed403c4e3a780db9935f633242855eac9c"
   name = "github.com/go-macaron/binding"
   name = "github.com/go-macaron/binding"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183"
   revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:6326b27f8e0c8e135c8674ddbc619fae879664ac832e8e6fa6a23ce0d279ed4d"
   name = "github.com/go-macaron/gzip"
   name = "github.com/go-macaron/gzip"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854"
   revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:fb8711b648d1ff03104fc1d9593a13cb1d5120be7ba2b01641c14ccae286a9e3"
   name = "github.com/go-macaron/inject"
   name = "github.com/go-macaron/inject"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "d8a0b8677191f4380287cfebd08e462217bac7ad"
   revision = "d8a0b8677191f4380287cfebd08e462217bac7ad"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:21577aafe885f088e8086a3415f154c63c0b7ce956a6994df2ac5776bc01b7e3"
   name = "github.com/go-macaron/session"
   name = "github.com/go-macaron/session"
   packages = [
   packages = [
     ".",
     ".",
     "memcache",
     "memcache",
     "postgres",
     "postgres",
-    "redis",
+    "redis"
   ]
   ]
-  pruneopts = "NUT"
   revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e"
   revision = "068d408f9c54c7fa7fcc5e2bdd3241ab21280c9e"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:fddd4bada6100d6fc49a9f32f18ba5718db45a58e4b00aa6377e1cfbf06af34f"
   name = "github.com/go-sql-driver/mysql"
   name = "github.com/go-sql-driver/mysql"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
   revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:a1efdbc2762667c8a41cbf02b19a0549c846bf2c1d08cad4f445e3344089f1f0"
   name = "github.com/go-stack/stack"
   name = "github.com/go-stack/stack"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
   revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
   version = "v1.7.0"
   version = "v1.7.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:b9d4f09cdaaa9e7dca5ed0b501ca5519eb2168dd01fc5d174c54edfe42a7d5ed"
   name = "github.com/go-xorm/builder"
   name = "github.com/go-xorm/builder"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "bad0a612f0d6277b953910822ab5dfb30dd18237"
   revision = "bad0a612f0d6277b953910822ab5dfb30dd18237"
   version = "v0.2.0"
   version = "v0.2.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:30fb106b0cd6d64ea6fccded579c8f7d788460092f885fcc8f3edd02fc2085a4"
   name = "github.com/go-xorm/core"
   name = "github.com/go-xorm/core"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "da1adaf7a28ca792961721a34e6e04945200c890"
   revision = "da1adaf7a28ca792961721a34e6e04945200c890"
   version = "v0.5.7"
   version = "v0.5.7"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:007d1354e4f44e6a393337e7623bcf911dfe75d6ef30fb767a6a0b65d302f5ed"
   name = "github.com/go-xorm/xorm"
   name = "github.com/go-xorm/xorm"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
   revision = "1933dd69e294c0a26c0266637067f24dbb25770c"
   version = "v0.6.4"
   version = "v0.6.4"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:ffbb19fb66f140b5ea059428d1f84246a055d1bc3d9456c1e5c3d143611f03d0"
   name = "github.com/golang/protobuf"
   name = "github.com/golang/protobuf"
   packages = [
   packages = [
     "proto",
     "proto",
     "ptypes",
     "ptypes",
     "ptypes/any",
     "ptypes/any",
     "ptypes/duration",
     "ptypes/duration",
-    "ptypes/timestamp",
+    "ptypes/timestamp"
   ]
   ]
-  pruneopts = "NUT"
   revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0"
   revision = "927b65914520a8b7d44f5c9057611cfec6b2e2d0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:f14d1b50e0075fb00177f12a96dd7addf93d1e2883c25befd17285b779549795"
   name = "github.com/gopherjs/gopherjs"
   name = "github.com/gopherjs/gopherjs"
   packages = ["js"]
   packages = ["js"]
-  pruneopts = "NUT"
   revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5"
   revision = "8dffc02ea1cb8398bb73f30424697c60fcf8d4c5"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:3b708ebf63bfa9ba3313bedb8526bc0bb284e51474e65e958481476a9d4a12aa"
   name = "github.com/gorilla/websocket"
   name = "github.com/gorilla/websocket"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
   revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
   version = "v1.2.0"
   version = "v1.2.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:4e771d1c6e15ca4516ad971c34205c822b5cff2747179679d7b321e4e1bfe431"
   name = "github.com/gosimple/slug"
   name = "github.com/gosimple/slug"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6"
   revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6"
   version = "v1.1.1"
   version = "v1.1.1"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:08e53c69cd267ef7d71eeae5d953153d0d2bc1b8e0b498731fe9acaead7001b6"
   name = "github.com/grafana/grafana-plugin-model"
   name = "github.com/grafana/grafana-plugin-model"
   packages = [
   packages = [
     "go/datasource",
     "go/datasource",
-    "go/renderer",
+    "go/renderer"
   ]
   ]
-  pruneopts = "NUT"
   revision = "84176c64269d8060f99e750ee8aba6f062753336"
   revision = "84176c64269d8060f99e750ee8aba6f062753336"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:58ba5285227b0f635652cd4aa82c4cfd00b590191eadd823462f0c9f64e3ae07"
   name = "github.com/hashicorp/go-hclog"
   name = "github.com/hashicorp/go-hclog"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "69ff559dc25f3b435631604f573a5fa1efdb6433"
   revision = "69ff559dc25f3b435631604f573a5fa1efdb6433"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:532090ffc3b05a7e4c0229dd2698d79149f2e0683df993224a8b202f607fb605"
   name = "github.com/hashicorp/go-plugin"
   name = "github.com/hashicorp/go-plugin"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "e8d22c780116115ae5624720c9af0c97afe4f551"
   revision = "e8d22c780116115ae5624720c9af0c97afe4f551"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:8925116d1edcd85fc0c014e1aa69ce12892489b48ee633a605c46d893b8c151f"
   name = "github.com/hashicorp/go-version"
   name = "github.com/hashicorp/go-version"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7"
   revision = "23480c0665776210b5fbbac6eaaee40e3e6a96b7"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:8deb0c5545c824dfeb0ac77ab8eb67a3d541eab76df5c85ce93064ef02d44cd0"
   name = "github.com/hashicorp/yamux"
   name = "github.com/hashicorp/yamux"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558"
   revision = "7221087c3d281fda5f794e28c2ea4c6e4d5c4558"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:efbe016b6d198cf44f1db0ed2fbdf1b36ebf1f6956cc9b76d6affa96f022d368"
   name = "github.com/inconshreveable/log15"
   name = "github.com/inconshreveable/log15"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
   revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
   version = "v2.13"
   version = "v2.13"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:ac6d01547ec4f7f673311b4663909269bfb8249952de3279799289467837c3cc"
   name = "github.com/jmespath/go-jmespath"
   name = "github.com/jmespath/go-jmespath"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "0b12d6b5"
   revision = "0b12d6b5"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:6ddab442e52381bab82fb6c07ef3f4b565ff7ec4b8fae96d8dd4b8573a460597"
   name = "github.com/jtolds/gls"
   name = "github.com/jtolds/gls"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
   revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
   version = "v4.2.1"
   version = "v4.2.1"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:1da1796a71eb70f1e3e085984d044f67840bb0326816ec8276231aa87b1b9fc3"
   name = "github.com/klauspost/compress"
   name = "github.com/klauspost/compress"
   packages = [
   packages = [
     "flate",
     "flate",
-    "gzip",
+    "gzip"
   ]
   ]
-  pruneopts = "NUT"
   revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
   revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
   version = "v1.2.1"
   version = "v1.2.1"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:5e55a8699c9ff7aba1e4c8952aeda209685d88d4cb63a8766c338e333b8e65d6"
   name = "github.com/klauspost/cpuid"
   name = "github.com/klauspost/cpuid"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
   revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
   version = "v1.1"
   version = "v1.1"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:b95da1293525625ef6f07be79d537b9bf2ecd7901efcf9a92193edafbd55b9ef"
   name = "github.com/klauspost/crc32"
   name = "github.com/klauspost/crc32"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
   revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
   version = "v1.1"
   version = "v1.1"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:7b21c7fc5551b46d1308b4ffa9e9e49b66c7a8b0ba88c0130474b0e7a20d859f"
   name = "github.com/kr/pretty"
   name = "github.com/kr/pretty"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
   revision = "73f6ac0b30a98e433b289500d779f50c1a6f0712"
   version = "v0.1.0"
   version = "v0.1.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:c3a7836b5904db0f8b609595b619916a6831cb35b8b714aec39f96d00c6155d8"
   name = "github.com/kr/text"
   name = "github.com/kr/text"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
   revision = "e2ffdb16a802fe2bb95e2e35ff34f0e53aeef34f"
   version = "v0.1.0"
   version = "v0.1.0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:7a1e592f0349d56fac8ce47f28469e4e7f4ce637cb26f40c88da9dff25db1c98"
   name = "github.com/lib/pq"
   name = "github.com/lib/pq"
   packages = [
   packages = [
     ".",
     ".",
-    "oid",
+    "oid"
   ]
   ]
-  pruneopts = "NUT"
   revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
   revision = "d34b9ff171c21ad295489235aec8b6626023cd04"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:08c231ec84231a7e23d67e4b58f975e1423695a32467a362ee55a803f9de8061"
   name = "github.com/mattn/go-colorable"
   name = "github.com/mattn/go-colorable"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
   revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
   version = "v0.0.9"
   version = "v0.0.9"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:bc4f7eec3b7be8c6cb1f0af6c1e3333d5bb71072951aaaae2f05067b0803f287"
   name = "github.com/mattn/go-isatty"
   name = "github.com/mattn/go-isatty"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
   revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
   version = "v0.0.3"
   version = "v0.0.3"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:536979f1c56397dbf91c2785159b37dec37e35d3bffa3cd1cfe66d25f51f8088"
   name = "github.com/mattn/go-sqlite3"
   name = "github.com/mattn/go-sqlite3"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
   revision = "323a32be5a2421b8c7087225079c6c900ec397cd"
   version = "v1.7.0"
   version = "v1.7.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:5985ef4caf91ece5d54817c11ea25f182697534f8ae6521eadcd628c142ac4b6"
   name = "github.com/matttproud/golang_protobuf_extensions"
   name = "github.com/matttproud/golang_protobuf_extensions"
   packages = ["pbutil"]
   packages = ["pbutil"]
-  pruneopts = "NUT"
   revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
   revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:18b773b92ac82a451c1276bd2776c1e55ce057ee202691ab33c8d6690efcc048"
   name = "github.com/mitchellh/go-testing-interface"
   name = "github.com/mitchellh/go-testing-interface"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
   revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:3b517122f3aad1ecce45a630ea912b3092b4729f25532a911d0cb2935a1f9352"
   name = "github.com/oklog/run"
   name = "github.com/oklog/run"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
   revision = "4dadeb3030eda0273a12382bb2348ffc7c9d1a39"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:7da29c22bcc5c2ffb308324377dc00b5084650348c2799e573ed226d8cc9faf0"
   name = "github.com/opentracing/opentracing-go"
   name = "github.com/opentracing/opentracing-go"
   packages = [
   packages = [
     ".",
     ".",
     "ext",
     "ext",
-    "log",
+    "log"
   ]
   ]
-  pruneopts = "NUT"
   revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
   revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
   version = "v1.0.2"
   version = "v1.0.2"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:748946761cf99c8b73cef5a3c0ee3e040859dd713a20cece0d0e0dc04e6ceca7"
   name = "github.com/patrickmn/go-cache"
   name = "github.com/patrickmn/go-cache"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
   revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
   version = "v2.1.0"
   version = "v2.1.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
   name = "github.com/pkg/errors"
   name = "github.com/pkg/errors"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
   revision = "645ef00459ed84a119197bfb8d8205042c6df63d"
   version = "v0.8.0"
   version = "v0.8.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:4759bed95e3a52febc18c071db28790a5c6e9e106ee201a37add6f6a056f8f9c"
   name = "github.com/prometheus/client_golang"
   name = "github.com/prometheus/client_golang"
   packages = [
   packages = [
     "api",
     "api",
     "api/prometheus/v1",
     "api/prometheus/v1",
     "prometheus",
     "prometheus",
-    "prometheus/promhttp",
+    "prometheus/promhttp"
   ]
   ]
-  pruneopts = "NUT"
   revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
   revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
   version = "v0.9.0-pre1"
   version = "v0.9.0-pre1"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:32d10bdfa8f09ecf13598324dba86ab891f11db3c538b6a34d1c3b5b99d7c36b"
   name = "github.com/prometheus/client_model"
   name = "github.com/prometheus/client_model"
   packages = ["go"]
   packages = ["go"]
-  pruneopts = "NUT"
   revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
   revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:768b555b86742de2f28beb37f1dedce9a75f91f871d75b5717c96399c1a78c08"
   name = "github.com/prometheus/common"
   name = "github.com/prometheus/common"
   packages = [
   packages = [
     "expfmt",
     "expfmt",
     "internal/bitbucket.org/ww/goautoneg",
     "internal/bitbucket.org/ww/goautoneg",
-    "model",
+    "model"
   ]
   ]
-  pruneopts = "NUT"
   revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
   revision = "d811d2e9bf898806ecfb6ef6296774b13ffc314c"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:c4a213a8d73fbb0b13f717ba7996116602ef18ecb42b91d77405877914cb0349"
   name = "github.com/prometheus/procfs"
   name = "github.com/prometheus/procfs"
   packages = [
   packages = [
     ".",
     ".",
     "internal/util",
     "internal/util",
     "nfs",
     "nfs",
-    "xfs",
+    "xfs"
   ]
   ]
-  pruneopts = "NUT"
   revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
   revision = "8b1c2da0d56deffdbb9e48d4414b4e674bd8083e"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:16e2136a67ec44aa2d1d6b0fd65394b3c4a8b2a1b6730c77967f7b7b06b179b2"
   name = "github.com/rainycape/unidecode"
   name = "github.com/rainycape/unidecode"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
   revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:d917313f309bda80d27274d53985bc65651f81a5b66b820749ac7f8ef061fd04"
   name = "github.com/sergi/go-diff"
   name = "github.com/sergi/go-diff"
   packages = ["diffmatchpatch"]
   packages = ["diffmatchpatch"]
-  pruneopts = "NUT"
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:1f0b284a6858827de4c27c66b49b2b25df3e16b031c2b57b7892273131e7dd2b"
   name = "github.com/smartystreets/assertions"
   name = "github.com/smartystreets/assertions"
   packages = [
   packages = [
     ".",
     ".",
     "internal/go-render/render",
     "internal/go-render/render",
-    "internal/oglematchers",
+    "internal/oglematchers"
   ]
   ]
-  pruneopts = "NUT"
   revision = "7678a5452ebea5b7090a6b163f844c133f523da2"
   revision = "7678a5452ebea5b7090a6b163f844c133f523da2"
   version = "1.8.3"
   version = "1.8.3"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:7efd0b2309cdd6468029fa30c808c50a820c9344df07e1a4bbdaf18f282907aa"
   name = "github.com/smartystreets/goconvey"
   name = "github.com/smartystreets/goconvey"
   packages = [
   packages = [
     "convey",
     "convey",
     "convey/gotest",
     "convey/gotest",
-    "convey/reporting",
+    "convey/reporting"
   ]
   ]
-  pruneopts = "NUT"
   revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
   revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
   version = "1.6.3"
   version = "1.6.3"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:a66add8dd963bfc72649017c1b321198f596cb4958cb1a11ff91a1be8691020b"
   name = "github.com/teris-io/shortid"
   name = "github.com/teris-io/shortid"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c"
   revision = "771a37caa5cf0c81f585d7b6df4dfc77e0615b5c"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:3d48c38e0eca8c66df62379c5ae7a83fb5cd839b94f241354c07ba077da7bc45"
   name = "github.com/uber/jaeger-client-go"
   name = "github.com/uber/jaeger-client-go"
   packages = [
   packages = [
     ".",
     ".",
@@ -604,55 +471,45 @@
     "thrift-gen/jaeger",
     "thrift-gen/jaeger",
     "thrift-gen/sampling",
     "thrift-gen/sampling",
     "thrift-gen/zipkincore",
     "thrift-gen/zipkincore",
-    "utils",
+    "utils"
   ]
   ]
-  pruneopts = "NUT"
   revision = "b043381d944715b469fd6b37addfd30145ca1758"
   revision = "b043381d944715b469fd6b37addfd30145ca1758"
   version = "v2.14.0"
   version = "v2.14.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:0f09db8429e19d57c8346ad76fbbc679341fa86073d3b8fb5ac919f0357d8f4c"
   name = "github.com/uber/jaeger-lib"
   name = "github.com/uber/jaeger-lib"
   packages = ["metrics"]
   packages = ["metrics"]
-  pruneopts = "NUT"
   revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5"
   revision = "ed3a127ec5fef7ae9ea95b01b542c47fbd999ce5"
   version = "v1.5.0"
   version = "v1.5.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:4c7d12ad3ef47bb03892a52e2609dc9a9cff93136ca9c7d31c00b79fcbc23c7b"
   name = "github.com/yudai/gojsondiff"
   name = "github.com/yudai/gojsondiff"
   packages = [
   packages = [
     ".",
     ".",
-    "formatter",
+    "formatter"
   ]
   ]
-  pruneopts = "NUT"
   revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
   revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
   version = "1.0.0"
   version = "1.0.0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:e50cbf8eba568d59b71e08c22c2a77809ed4646ae06ef4abb32b3d3d3fdb1a77"
   name = "github.com/yudai/golcs"
   name = "github.com/yudai/golcs"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
   revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:758f363e0dff33cf00b234be2efb12f919d79b42d5ae3909ff9eb69ef2c3cca5"
   name = "golang.org/x/crypto"
   name = "golang.org/x/crypto"
   packages = [
   packages = [
     "ed25519",
     "ed25519",
     "ed25519/internal/edwards25519",
     "ed25519/internal/edwards25519",
     "md4",
     "md4",
-    "pbkdf2",
+    "pbkdf2"
   ]
   ]
-  pruneopts = "NUT"
   revision = "1a580b3eff7814fc9b40602fd35256c63b50f491"
   revision = "1a580b3eff7814fc9b40602fd35256c63b50f491"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:0b3fee9c4472022a0982ee0d81e08b3cc3e595f50befd7a4b358b48540d9d8c5"
   name = "golang.org/x/net"
   name = "golang.org/x/net"
   packages = [
   packages = [
     "context",
     "context",
@@ -662,43 +519,35 @@
     "http2/hpack",
     "http2/hpack",
     "idna",
     "idna",
     "internal/timeseries",
     "internal/timeseries",
-    "trace",
+    "trace"
   ]
   ]
-  pruneopts = "NUT"
   revision = "2491c5de3490fced2f6cff376127c667efeed857"
   revision = "2491c5de3490fced2f6cff376127c667efeed857"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:46bd4e66bfce5e77f08fc2e8dcacc3676e679241ce83d9c150ff0397d686dd44"
   name = "golang.org/x/oauth2"
   name = "golang.org/x/oauth2"
   packages = [
   packages = [
     ".",
     ".",
     "google",
     "google",
     "internal",
     "internal",
     "jws",
     "jws",
-    "jwt",
+    "jwt"
   ]
   ]
-  pruneopts = "NUT"
   revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269"
   revision = "cdc340f7c179dbbfa4afd43b7614e8fcadde4269"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:39ebcc2b11457b703ae9ee2e8cca0f68df21969c6102cb3b705f76cca0ea0239"
   name = "golang.org/x/sync"
   name = "golang.org/x/sync"
   packages = ["errgroup"]
   packages = ["errgroup"]
-  pruneopts = "NUT"
   revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
   revision = "1d60e4601c6fd243af51cc01ddf169918a5407ca"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:ec21c5bf0572488865b93e30ffd9132afbf85bec0b20c2d6cbcf349cf2031ed5"
   name = "golang.org/x/sys"
   name = "golang.org/x/sys"
   packages = ["unix"]
   packages = ["unix"]
-  pruneopts = "NUT"
   revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b"
   revision = "7c87d13f8e835d2fb3a70a2912c811ed0c1d241b"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:e7071ed636b5422cc51c0e3a6cebc229d6c9fffc528814b519a980641422d619"
   name = "golang.org/x/text"
   name = "golang.org/x/text"
   packages = [
   packages = [
     "collate",
     "collate",
@@ -714,14 +563,12 @@
     "unicode/bidi",
     "unicode/bidi",
     "unicode/cldr",
     "unicode/cldr",
     "unicode/norm",
     "unicode/norm",
-    "unicode/rangetable",
+    "unicode/rangetable"
   ]
   ]
-  pruneopts = "NUT"
   revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
   revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0"
   version = "v0.3.0"
   version = "v0.3.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:dbd5568923513ee74aa626d027e2a8a352cf8f35df41d19f4e34491d1858c38b"
   name = "google.golang.org/appengine"
   name = "google.golang.org/appengine"
   packages = [
   packages = [
     ".",
     ".",
@@ -734,22 +581,18 @@
     "internal/modules",
     "internal/modules",
     "internal/remote_api",
     "internal/remote_api",
     "internal/urlfetch",
     "internal/urlfetch",
-    "urlfetch",
+    "urlfetch"
   ]
   ]
-  pruneopts = "NUT"
   revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
   revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
   version = "v1.0.0"
   version = "v1.0.0"
 
 
 [[projects]]
 [[projects]]
   branch = "master"
   branch = "master"
-  digest = "1:3c24554c312721e98fa6b76403e7100cf974eb46b1255ea7fc6471db9a9ce498"
   name = "google.golang.org/genproto"
   name = "google.golang.org/genproto"
   packages = ["googleapis/rpc/status"]
   packages = ["googleapis/rpc/status"]
-  pruneopts = "NUT"
   revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff"
   revision = "7bb2a897381c9c5ab2aeb8614f758d7766af68ff"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:840b77b6eb539b830bb760b6e30b688ed2ff484bd83466fce2395835ed9367fe"
   name = "google.golang.org/grpc"
   name = "google.golang.org/grpc"
   packages = [
   packages = [
     ".",
     ".",
@@ -776,167 +619,78 @@
     "stats",
     "stats",
     "status",
     "status",
     "tap",
     "tap",
-    "transport",
+    "transport"
   ]
   ]
-  pruneopts = "NUT"
   revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34"
   revision = "1e2570b1b19ade82d8dbb31bba4e65e9f9ef5b34"
   version = "v1.11.1"
   version = "v1.11.1"
 
 
 [[projects]]
 [[projects]]
   branch = "v3"
   branch = "v3"
-  digest = "1:1244a9b3856f70d5ffb74bbfd780fc9d47f93f2049fa265c6fb602878f507bf8"
   name = "gopkg.in/alexcesaro/quotedprintable.v3"
   name = "gopkg.in/alexcesaro/quotedprintable.v3"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "2caba252f4dc53eaf6b553000885530023f54623"
   revision = "2caba252f4dc53eaf6b553000885530023f54623"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:aea6e9483c167cc6fdf1274c442558c5dda8fd3373372be04d98c79100868da1"
   name = "gopkg.in/asn1-ber.v1"
   name = "gopkg.in/asn1-ber.v1"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "379148ca0225df7a432012b8df0355c2a2063ac0"
   revision = "379148ca0225df7a432012b8df0355c2a2063ac0"
   version = "v1.2"
   version = "v1.2"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:24bfc2e8bf971485cb5ba0f0e5b08a1b806cca5828134df76b32d1ea50f2ab49"
   name = "gopkg.in/bufio.v1"
   name = "gopkg.in/bufio.v1"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
   revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
   version = "v1"
   version = "v1"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:e05711632e1515319b014e8fe4cbe1d30ab024c473403f60cf0fdeb4c586a474"
   name = "gopkg.in/ini.v1"
   name = "gopkg.in/ini.v1"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
   revision = "6529cf7c58879c08d927016dde4477f18a0634cb"
   version = "v1.36.0"
   version = "v1.36.0"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:3b0cf3a465fd07f76e5fc1a9d0783c662dac0de9fc73d713ebe162768fd87b5f"
+  name = "gopkg.in/ldap.v3"
+  packages = ["."]
+  revision = "5c2c0f997205c29de14cb6c35996370c2c5dfab1"
+  version = "v3"
+
+[[projects]]
   name = "gopkg.in/macaron.v1"
   name = "gopkg.in/macaron.v1"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10"
   revision = "c1be95e6d21e769e44e1ec33cec9da5837861c10"
   version = "v1.3.1"
   version = "v1.3.1"
 
 
 [[projects]]
 [[projects]]
   branch = "v2"
   branch = "v2"
-  digest = "1:d52332f9e9f2c6343652e13aa3fd40cfd03353520c9a48d90f21215d3012d50f"
   name = "gopkg.in/mail.v2"
   name = "gopkg.in/mail.v2"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68"
   revision = "5bc5c8bb07bd8d2803831fbaf8cbd630fcde2c68"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:00126f697efdcab42f07c89ac8bf0095fb2328aef6464e070055154088cea859"
   name = "gopkg.in/redis.v2"
   name = "gopkg.in/redis.v2"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "e6179049628164864e6e84e973cfb56335748dea"
   revision = "e6179049628164864e6e84e973cfb56335748dea"
   version = "v2.3.2"
   version = "v2.3.2"
 
 
 [[projects]]
 [[projects]]
-  digest = "1:a50fabe7a46692dc7c656310add3d517abe7914df02afd151ef84da884605dc8"
   name = "gopkg.in/square/go-jose.v2"
   name = "gopkg.in/square/go-jose.v2"
   packages = [
   packages = [
     ".",
     ".",
     "cipher",
     "cipher",
-    "json",
+    "json"
   ]
   ]
-  pruneopts = "NUT"
   revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
   revision = "ef984e69dd356202fd4e4910d4d9c24468bdf0b8"
   version = "v2.1.9"
   version = "v2.1.9"
 
 
 [[projects]]
 [[projects]]
   branch = "v2"
   branch = "v2"
-  digest = "1:7c95b35057a0ff2e19f707173cc1a947fa43a6eb5c4d300d196ece0334046082"
   name = "gopkg.in/yaml.v2"
   name = "gopkg.in/yaml.v2"
   packages = ["."]
   packages = ["."]
-  pruneopts = "NUT"
   revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
   revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183"
 
 
 [solve-meta]
 [solve-meta]
   analyzer-name = "dep"
   analyzer-name = "dep"
   analyzer-version = 1
   analyzer-version = 1
-  input-imports = [
-    "github.com/BurntSushi/toml",
-    "github.com/Unknwon/com",
-    "github.com/VividCortex/mysqlerr",
-    "github.com/aws/aws-sdk-go/aws",
-    "github.com/aws/aws-sdk-go/aws/awserr",
-    "github.com/aws/aws-sdk-go/aws/awsutil",
-    "github.com/aws/aws-sdk-go/aws/credentials",
-    "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds",
-    "github.com/aws/aws-sdk-go/aws/credentials/endpointcreds",
-    "github.com/aws/aws-sdk-go/aws/defaults",
-    "github.com/aws/aws-sdk-go/aws/ec2metadata",
-    "github.com/aws/aws-sdk-go/aws/endpoints",
-    "github.com/aws/aws-sdk-go/aws/request",
-    "github.com/aws/aws-sdk-go/aws/session",
-    "github.com/aws/aws-sdk-go/service/cloudwatch",
-    "github.com/aws/aws-sdk-go/service/ec2",
-    "github.com/aws/aws-sdk-go/service/ec2/ec2iface",
-    "github.com/aws/aws-sdk-go/service/s3",
-    "github.com/aws/aws-sdk-go/service/sts",
-    "github.com/benbjohnson/clock",
-    "github.com/bmizerany/assert",
-    "github.com/codegangsta/cli",
-    "github.com/davecgh/go-spew/spew",
-    "github.com/denisenkom/go-mssqldb",
-    "github.com/facebookgo/inject",
-    "github.com/fatih/color",
-    "github.com/go-ldap/ldap",
-    "github.com/go-macaron/binding",
-    "github.com/go-macaron/gzip",
-    "github.com/go-macaron/session",
-    "github.com/go-macaron/session/memcache",
-    "github.com/go-macaron/session/postgres",
-    "github.com/go-macaron/session/redis",
-    "github.com/go-sql-driver/mysql",
-    "github.com/go-stack/stack",
-    "github.com/go-xorm/core",
-    "github.com/go-xorm/xorm",
-    "github.com/gorilla/websocket",
-    "github.com/gosimple/slug",
-    "github.com/grafana/grafana-plugin-model/go/datasource",
-    "github.com/grafana/grafana-plugin-model/go/renderer",
-    "github.com/hashicorp/go-hclog",
-    "github.com/hashicorp/go-plugin",
-    "github.com/hashicorp/go-version",
-    "github.com/inconshreveable/log15",
-    "github.com/lib/pq",
-    "github.com/mattn/go-isatty",
-    "github.com/mattn/go-sqlite3",
-    "github.com/opentracing/opentracing-go",
-    "github.com/opentracing/opentracing-go/ext",
-    "github.com/opentracing/opentracing-go/log",
-    "github.com/patrickmn/go-cache",
-    "github.com/pkg/errors",
-    "github.com/prometheus/client_golang/api",
-    "github.com/prometheus/client_golang/api/prometheus/v1",
-    "github.com/prometheus/client_golang/prometheus",
-    "github.com/prometheus/client_golang/prometheus/promhttp",
-    "github.com/prometheus/client_model/go",
-    "github.com/prometheus/common/expfmt",
-    "github.com/prometheus/common/model",
-    "github.com/smartystreets/goconvey/convey",
-    "github.com/teris-io/shortid",
-    "github.com/uber/jaeger-client-go/config",
-    "github.com/yudai/gojsondiff",
-    "github.com/yudai/gojsondiff/formatter",
-    "golang.org/x/net/context/ctxhttp",
-    "golang.org/x/oauth2",
-    "golang.org/x/oauth2/google",
-    "golang.org/x/oauth2/jwt",
-    "golang.org/x/sync/errgroup",
-    "gopkg.in/ini.v1",
-    "gopkg.in/macaron.v1",
-    "gopkg.in/mail.v2",
-    "gopkg.in/square/go-jose.v2",
-    "gopkg.in/yaml.v2",
-  ]
+  inputs-digest = "88f0eb826b9c154ba46ea3bb64767707d86db75449ec75199eb2b8cf2b337fd4"
   solver-name = "gps-cdcl"
   solver-name = "gps-cdcl"
   solver-version = 1
   solver-version = 1

+ 4 - 4
Gopkg.toml

@@ -58,10 +58,6 @@ ignored = [
   name = "github.com/fatih/color"
   name = "github.com/fatih/color"
   version = "1.5.0"
   version = "1.5.0"
 
 
-[[constraint]]
-  name = "github.com/go-ldap/ldap"
-  version = "2.5.1"
-
 [[constraint]]
 [[constraint]]
   branch = "master"
   branch = "master"
   name = "github.com/go-macaron/binding"
   name = "github.com/go-macaron/binding"
@@ -211,3 +207,7 @@ ignored = [
 [[constraint]]
 [[constraint]]
   name = "gopkg.in/square/go-jose.v2"
   name = "gopkg.in/square/go-jose.v2"
   version = "2.1.9"
   version = "2.1.9"
+
+[[constraint]]
+  name = "gopkg.in/ldap.v3"
+  version = "3.0.0"

+ 3 - 3
README.md

@@ -25,7 +25,7 @@ the latest master builds [here](https://grafana.com/grafana/download)
 ### Dependencies
 ### Dependencies
 
 
 - Go (Latest Stable)
 - Go (Latest Stable)
-- NodeJS LTS
+- Node.js LTS
 
 
 ### Building the backend
 ### Building the backend
 ```bash
 ```bash
@@ -37,7 +37,7 @@ go run build.go build
 
 
 ### Building frontend assets
 ### Building frontend assets
 
 
-For this you need nodejs (v.6+).
+For this you need Node.js (LTS version).
 
 
 To build the assets, rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
 To build the assets, rebuild on file change, and serve them by Grafana's webserver (http://localhost:3000):
 ```bash
 ```bash
@@ -90,7 +90,7 @@ Choose this option to build on platforms other than linux/amd64 and/or not have
 
 
 The resulting image will be tagged as `grafana/grafana:dev`
 The resulting image will be tagged as `grafana/grafana:dev`
 
 
-Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Perferences -> Advanced), otherwize you may faild at `grunt build`
+Notice: If you are using Docker for MacOS, be sure to let limit of Memory bigger than 2 GiB (at docker -> Preferences -> Advanced), otherwize you may faild at `grunt build`
 
 
 ### Dev config
 ### Dev config
 
 

+ 1 - 1
appveyor.yml

@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
 environment:
 environment:
   nodejs_version: "8"
   nodejs_version: "8"
   GOPATH: C:\gopath
   GOPATH: C:\gopath
-  GOVERSION: 1.11
+  GOVERSION: 1.11.4
 
 
 install:
 install:
   - rmdir c:\go /s /q
   - rmdir c:\go /s /q

+ 1 - 0
conf/defaults.ini

@@ -335,6 +335,7 @@ tls_skip_verify_insecure = false
 tls_client_cert =
 tls_client_cert =
 tls_client_key =
 tls_client_key =
 tls_client_ca =
 tls_client_ca =
+send_client_credentials_via_post = false
 
 
 #################################### Basic Auth ##########################
 #################################### Basic Auth ##########################
 [auth.basic]
 [auth.basic]

+ 4 - 0
conf/sample.ini

@@ -284,6 +284,10 @@ log_queries =
 ;tls_client_key =
 ;tls_client_key =
 ;tls_client_ca =
 ;tls_client_ca =
 
 
+; Set to true to enable sending client_id and client_secret via POST body instead of Basic authentication HTTP header
+; This might be required if the OAuth provider is not RFC6749 compliant, only supporting credentials passed via POST payload
+;send_client_credentials_via_post = false
+
 #################################### Grafana.com Auth ####################
 #################################### Grafana.com Auth ####################
 [auth.grafana_com]
 [auth.grafana_com]
 ;enabled = false
 ;enabled = false

+ 12 - 1
docs/sources/auth/generic-oauth.md

@@ -17,7 +17,7 @@ can find examples using Okta, BitBucket, OneLogin and Azure.
 
 
 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/generic_oauth`.
 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/generic_oauth`.
 
 
-You may have to set the `root_url` option of `[server]` for the callback URL to be 
+You may have to set the `root_url` option of `[server]` for the callback URL to be
 correct. For example in case you are serving Grafana behind a proxy.
 correct. For example in case you are serving Grafana behind a proxy.
 
 
 Example config:
 Example config:
@@ -209,6 +209,17 @@ allowed_organizations =
     token_url = https://<your domain>.my.centrify.com/OAuth2/Token/<Application ID>
     token_url = https://<your domain>.my.centrify.com/OAuth2/Token/<Application ID>
     ```
     ```
 
 
+## Set up OAuth2 with non-compliant providers
+
+Some OAuth2 providers might not support `client_id` and `client_secret` passed via Basic Authentication HTTP header, which
+results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be
+send via POST body, which can be enabled via the following settings:
+
+    ```bash
+    [auth.generic_oauth]
+    send_client_credentials_via_post = true
+    ```
+
 <hr>
 <hr>
 
 
 
 

+ 1 - 1
docs/sources/auth/github.md

@@ -1,5 +1,5 @@
 +++
 +++
-title = "Google OAuth2 Authentication"
+title = "GitHub OAuth2 Authentication"
 description = "Grafana OAuthentication Guide "
 description = "Grafana OAuthentication Guide "
 keywords = ["grafana", "configuration", "documentation", "oauth"]
 keywords = ["grafana", "configuration", "documentation", "oauth"]
 type = "docs"
 type = "docs"

+ 1 - 1
docs/sources/auth/gitlab.md

@@ -1,5 +1,5 @@
 +++
 +++
-title = "Google OAuth2 Authentication"
+title = "GitLab OAuth2 Authentication"
 description = "Grafana OAuthentication Guide "
 description = "Grafana OAuthentication Guide "
 keywords = ["grafana", "configuration", "documentation", "oauth"]
 keywords = ["grafana", "configuration", "documentation", "oauth"]
 type = "docs"
 type = "docs"

+ 9 - 7
package.json

@@ -20,9 +20,9 @@
     "@types/enzyme": "^3.1.13",
     "@types/enzyme": "^3.1.13",
     "@types/jest": "^23.3.2",
     "@types/jest": "^23.3.2",
     "@types/node": "^8.0.31",
     "@types/node": "^8.0.31",
-    "@types/react": "^16.4.14",
+    "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-custom-scrollbars": "^4.0.5",
-    "@types/react-dom": "^16.0.7",
+    "@types/react-dom": "^16.0.9",
     "@types/react-select": "^2.0.4",
     "@types/react-select": "^2.0.4",
     "angular-mocks": "1.6.6",
     "angular-mocks": "1.6.6",
     "autoprefixer": "^6.4.0",
     "autoprefixer": "^6.4.0",
@@ -148,17 +148,18 @@
     "prismjs": "^1.6.0",
     "prismjs": "^1.6.0",
     "prop-types": "^15.6.2",
     "prop-types": "^15.6.2",
     "rc-cascader": "^0.14.0",
     "rc-cascader": "^0.14.0",
-    "react": "^16.5.0",
+    "react": "^16.6.3",
     "react-custom-scrollbars": "^4.2.1",
     "react-custom-scrollbars": "^4.2.1",
-    "react-dom": "^16.5.0",
+    "react-dom": "^16.6.3",
     "react-grid-layout": "0.16.6",
     "react-grid-layout": "0.16.6",
+    "react-popper": "^1.3.0",
     "react-highlight-words": "0.11.0",
     "react-highlight-words": "0.11.0",
-    "react-popper": "^0.7.5",
     "react-redux": "^5.0.7",
     "react-redux": "^5.0.7",
-    "react-select": "2.1.0",
+    "@torkelo/react-select": "2.1.1",
     "react-sizeme": "^2.3.6",
     "react-sizeme": "^2.3.6",
     "react-table": "^6.8.6",
     "react-table": "^6.8.6",
     "react-transition-group": "^2.2.1",
     "react-transition-group": "^2.2.1",
+    "react-virtualized": "^9.21.0",
     "redux": "^4.0.0",
     "redux": "^4.0.0",
     "redux-logger": "^3.0.6",
     "redux-logger": "^3.0.6",
     "redux-thunk": "^2.3.0",
     "redux-thunk": "^2.3.0",
@@ -175,6 +176,7 @@
     "tslint-react": "^3.6.0"
     "tslint-react": "^3.6.0"
   },
   },
   "resolutions": {
   "resolutions": {
-    "caniuse-db": "1.0.30000772"
+    "caniuse-db": "1.0.30000772",
+    "**/@types/react": "16.7.6"
   }
   }
 }
 }

+ 11 - 11
packaging/publish/publish_both.sh

@@ -1,17 +1,17 @@
 #! /usr/bin/env bash
 #! /usr/bin/env bash
-version=5.4.1
+version=5.4.2
 
 
-wget https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb
+# wget https://dl.grafana.com/oss/release/grafana_${version}_amd64.deb
+#
+# package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb
+# package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb
+# package_cloud push grafana/stable/debian/stretch grafana_${version}_amd64.deb
+#
+# package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb
+# package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb --verbose
+# package_cloud push grafana/testing/debian/stretch grafana_${version}_amd64.deb --verbose
 
 
-package_cloud push grafana/stable/debian/jessie grafana_${version}_amd64.deb
-package_cloud push grafana/stable/debian/wheezy grafana_${version}_amd64.deb
-package_cloud push grafana/stable/debian/stretch grafana_${version}_amd64.deb
-
-package_cloud push grafana/testing/debian/jessie grafana_${version}_amd64.deb
-package_cloud push grafana/testing/debian/wheezy grafana_${version}_amd64.deb --verbose
-package_cloud push grafana/testing/debian/stretch grafana_${version}_amd64.deb --verbose
-
-wget https://dl.grafana.com/release/grafana-${version}-1.x86_64.rpm
+wget https://dl.grafana.com/oss/release/grafana-${version}-1.x86_64.rpm
 
 
 package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm --verbose
 package_cloud push grafana/testing/el/6 grafana-${version}-1.x86_64.rpm --verbose
 package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm --verbose
 package_cloud push grafana/testing/el/7 grafana-${version}-1.x86_64.rpm --verbose

+ 112 - 8
pkg/api/dashboard_snapshot.go

@@ -1,10 +1,15 @@
 package api
 package api
 
 
 import (
 import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/api/dtos"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/guardian"
 	"github.com/grafana/grafana/pkg/services/guardian"
@@ -12,6 +17,11 @@ import (
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
 
 
+var client = &http.Client{
+	Timeout:   time.Second * 5,
+	Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
+}
+
 func GetSharingOptions(c *m.ReqContext) {
 func GetSharingOptions(c *m.ReqContext) {
 	c.JSON(200, util.DynMap{
 	c.JSON(200, util.DynMap{
 		"externalSnapshotURL":  setting.ExternalSnapshotUrl,
 		"externalSnapshotURL":  setting.ExternalSnapshotUrl,
@@ -20,26 +30,79 @@ func GetSharingOptions(c *m.ReqContext) {
 	})
 	})
 }
 }
 
 
+type CreateExternalSnapshotResponse struct {
+	Key       string `json:"key"`
+	DeleteKey string `json:"deleteKey"`
+	Url       string `json:"url"`
+	DeleteUrl string `json:"deleteUrl"`
+}
+
+func createExternalDashboardSnapshot(cmd m.CreateDashboardSnapshotCommand) (*CreateExternalSnapshotResponse, error) {
+	var createSnapshotResponse CreateExternalSnapshotResponse
+	message := map[string]interface{}{
+		"name":      cmd.Name,
+		"expires":   cmd.Expires,
+		"dashboard": cmd.Dashboard,
+	}
+
+	messageBytes, err := simplejson.NewFromAny(message).Encode()
+	if err != nil {
+		return nil, err
+	}
+
+	response, err := client.Post(setting.ExternalSnapshotUrl+"/api/snapshots", "application/json", bytes.NewBuffer(messageBytes))
+	if err != nil {
+		return nil, err
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode != 200 {
+		return nil, fmt.Errorf("Create external snapshot response status code %d", response.StatusCode)
+	}
+
+	if err := json.NewDecoder(response.Body).Decode(&createSnapshotResponse); err != nil {
+		return nil, err
+	}
+
+	return &createSnapshotResponse, nil
+}
+
+// POST /api/snapshots
 func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) {
 func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotCommand) {
 	if cmd.Name == "" {
 	if cmd.Name == "" {
 		cmd.Name = "Unnamed snapshot"
 		cmd.Name = "Unnamed snapshot"
 	}
 	}
 
 
+	var url string
+	cmd.ExternalUrl = ""
+	cmd.OrgId = c.OrgId
+	cmd.UserId = c.UserId
+
 	if cmd.External {
 	if cmd.External {
-		// external snapshot ref requires key and delete key
-		if cmd.Key == "" || cmd.DeleteKey == "" {
-			c.JsonApiErr(400, "Missing key and delete key for external snapshot", nil)
+		if !setting.ExternalEnabled {
+			c.JsonApiErr(403, "External dashboard creation is disabled", nil)
+			return
+		}
+
+		response, err := createExternalDashboardSnapshot(cmd)
+		if err != nil {
+			c.JsonApiErr(500, "Failed to create external snaphost", err)
 			return
 			return
 		}
 		}
 
 
-		cmd.OrgId = -1
-		cmd.UserId = -1
+		url = response.Url
+		cmd.Key = response.Key
+		cmd.DeleteKey = response.DeleteKey
+		cmd.ExternalUrl = response.Url
+		cmd.ExternalDeleteUrl = response.DeleteUrl
+		cmd.Dashboard = simplejson.New()
+
 		metrics.M_Api_Dashboard_Snapshot_External.Inc()
 		metrics.M_Api_Dashboard_Snapshot_External.Inc()
 	} else {
 	} else {
 		cmd.Key = util.GetRandomString(32)
 		cmd.Key = util.GetRandomString(32)
 		cmd.DeleteKey = util.GetRandomString(32)
 		cmd.DeleteKey = util.GetRandomString(32)
-		cmd.OrgId = c.OrgId
-		cmd.UserId = c.UserId
+		url = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
+
 		metrics.M_Api_Dashboard_Snapshot_Create.Inc()
 		metrics.M_Api_Dashboard_Snapshot_Create.Inc()
 	}
 	}
 
 
@@ -51,7 +114,7 @@ func CreateDashboardSnapshot(c *m.ReqContext, cmd m.CreateDashboardSnapshotComma
 	c.JSON(200, util.DynMap{
 	c.JSON(200, util.DynMap{
 		"key":       cmd.Key,
 		"key":       cmd.Key,
 		"deleteKey": cmd.DeleteKey,
 		"deleteKey": cmd.DeleteKey,
-		"url":       setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key),
+		"url":       url,
 		"deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey),
 		"deleteUrl": setting.ToAbsUrl("api/snapshots-delete/" + cmd.DeleteKey),
 	})
 	})
 }
 }
@@ -91,6 +154,33 @@ func GetDashboardSnapshot(c *m.ReqContext) {
 	c.JSON(200, dto)
 	c.JSON(200, dto)
 }
 }
 
 
+func deleteExternalDashboardSnapshot(externalUrl string) error {
+	response, err := client.Get(externalUrl)
+	if err != nil {
+		return err
+	}
+	defer response.Body.Close()
+
+	if response.StatusCode == 200 {
+		return nil
+	}
+
+	// Gracefully ignore "snapshot not found" errors as they could have already
+	// been removed either via the cleanup script or by request.
+	if response.StatusCode == 500 {
+		var respJson map[string]interface{}
+		if err := json.NewDecoder(response.Body).Decode(&respJson); err != nil {
+			return err
+		}
+
+		if respJson["message"] == "Failed to get dashboard snapshot" {
+			return nil
+		}
+	}
+
+	return fmt.Errorf("Unexpected response when deleting external snapshot. Status code: %d", response.StatusCode)
+}
+
 // GET /api/snapshots-delete/:deleteKey
 // GET /api/snapshots-delete/:deleteKey
 func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response {
 func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response {
 	key := c.Params(":deleteKey")
 	key := c.Params(":deleteKey")
@@ -102,6 +192,13 @@ func DeleteDashboardSnapshotByDeleteKey(c *m.ReqContext) Response {
 		return Error(500, "Failed to get dashboard snapshot", err)
 		return Error(500, "Failed to get dashboard snapshot", err)
 	}
 	}
 
 
+	if query.Result.External {
+		err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl)
+		if err != nil {
+			return Error(500, "Failed to delete external dashboard", err)
+		}
+	}
+
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
 
 
 	if err := bus.Dispatch(cmd); err != nil {
 	if err := bus.Dispatch(cmd); err != nil {
@@ -138,6 +235,13 @@ func DeleteDashboardSnapshot(c *m.ReqContext) Response {
 		return Error(403, "Access denied to this snapshot", nil)
 		return Error(403, "Access denied to this snapshot", nil)
 	}
 	}
 
 
+	if query.Result.External {
+		err := deleteExternalDashboardSnapshot(query.Result.ExternalDeleteUrl)
+		if err != nil {
+			return Error(500, "Failed to delete external dashboard", err)
+		}
+	}
+
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
 	cmd := &m.DeleteDashboardSnapshotCommand{DeleteKey: query.Result.DeleteKey}
 
 
 	if err := bus.Dispatch(cmd); err != nil {
 	if err := bus.Dispatch(cmd); err != nil {

+ 87 - 0
pkg/api/dashboard_snapshot_test.go

@@ -1,6 +1,9 @@
 package api
 package api
 
 
 import (
 import (
+	"fmt"
+	"net/http"
+	"net/http/httptest"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -13,13 +16,17 @@ import (
 
 
 func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 	Convey("Given a single snapshot", t, func() {
 	Convey("Given a single snapshot", t, func() {
+		var externalRequest *http.Request
 		jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`))
 		jsonModel, _ := simplejson.NewJson([]byte(`{"id":100}`))
 
 
 		mockSnapshotResult := &m.DashboardSnapshot{
 		mockSnapshotResult := &m.DashboardSnapshot{
 			Id:        1,
 			Id:        1,
+			Key:       "12345",
+			DeleteKey: "54321",
 			Dashboard: jsonModel,
 			Dashboard: jsonModel,
 			Expires:   time.Now().Add(time.Duration(1000) * time.Second),
 			Expires:   time.Now().Add(time.Duration(1000) * time.Second),
 			UserId:    999999,
 			UserId:    999999,
+			External:  true,
 		}
 		}
 
 
 		bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error {
 		bus.AddHandler("test", func(query *m.GetDashboardSnapshotQuery) error {
@@ -45,13 +52,25 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 			return nil
 			return nil
 		})
 		})
 
 
+		setupRemoteServer := func(fn func(http.ResponseWriter, *http.Request)) *httptest.Server {
+			return httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
+				fn(rw, r)
+			}))
+		}
+
 		Convey("When user has editor role and is not in the ACL", func() {
 		Convey("When user has editor role and is not in the ACL", func() {
 			Convey("Should not be able to delete snapshot", func() {
 			Convey("Should not be able to delete snapshot", func() {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						externalRequest = req
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
 					sc.handlerFunc = DeleteDashboardSnapshot
 					sc.handlerFunc = DeleteDashboardSnapshot
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
 
 
 					So(sc.resp.Code, ShouldEqual, 403)
 					So(sc.resp.Code, ShouldEqual, 403)
+					So(externalRequest, ShouldBeNil)
 				})
 				})
 			})
 			})
 		})
 		})
@@ -59,6 +78,12 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 		Convey("When user is anonymous", func() {
 		Convey("When user is anonymous", func() {
 			Convey("Should be able to delete snapshot by deleteKey", func() {
 			Convey("Should be able to delete snapshot by deleteKey", func() {
 				anonymousUserScenario("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) {
 				anonymousUserScenario("When calling GET on", "GET", "/api/snapshots-delete/12345", "/api/snapshots-delete/:deleteKey", func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						rw.WriteHeader(200)
+						externalRequest = req
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
 					sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey
 					sc.handlerFunc = DeleteDashboardSnapshotByDeleteKey
 					sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec()
 					sc.fakeReqWithParams("GET", sc.url, map[string]string{"deleteKey": "12345"}).exec()
 
 
@@ -67,6 +92,10 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
 					So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
+
+					So(externalRequest.Method, ShouldEqual, http.MethodGet)
+					So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL)
+					So(externalRequest.URL.EscapedPath(), ShouldEqual, "/")
 				})
 				})
 			})
 			})
 		})
 		})
@@ -79,6 +108,12 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 
 
 			Convey("Should be able to delete a snapshot", func() {
 			Convey("Should be able to delete a snapshot", func() {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						rw.WriteHeader(200)
+						externalRequest = req
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
 					sc.handlerFunc = DeleteDashboardSnapshot
 					sc.handlerFunc = DeleteDashboardSnapshot
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
 					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
 
 
@@ -87,6 +122,8 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 					So(err, ShouldBeNil)
 					So(err, ShouldBeNil)
 
 
 					So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
 					So(respJSON.Get("message").MustString(), ShouldStartWith, "Snapshot deleted")
+					So(fmt.Sprintf("http://%s", externalRequest.Host), ShouldEqual, ts.URL)
+					So(externalRequest.URL.EscapedPath(), ShouldEqual, "/")
 				})
 				})
 			})
 			})
 		})
 		})
@@ -94,6 +131,7 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 		Convey("When user is editor and is the creator of the snapshot", func() {
 		Convey("When user is editor and is the creator of the snapshot", func() {
 			aclMockResp = []*m.DashboardAclInfoDTO{}
 			aclMockResp = []*m.DashboardAclInfoDTO{}
 			mockSnapshotResult.UserId = TestUserID
 			mockSnapshotResult.UserId = TestUserID
+			mockSnapshotResult.External = false
 
 
 			Convey("Should be able to delete a snapshot", func() {
 			Convey("Should be able to delete a snapshot", func() {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
 				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
@@ -108,5 +146,54 @@ func TestDashboardSnapshotApiEndpoint(t *testing.T) {
 				})
 				})
 			})
 			})
 		})
 		})
+
+		Convey("When deleting an external snapshot", func() {
+			aclMockResp = []*m.DashboardAclInfoDTO{}
+			mockSnapshotResult.UserId = TestUserID
+
+			Convey("Should gracefully delete local snapshot when remote snapshot has already been removed", func() {
+				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						rw.Write([]byte(`{"message":"Failed to get dashboard snapshot"}`))
+						rw.WriteHeader(500)
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
+					sc.handlerFunc = DeleteDashboardSnapshot
+					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
+
+					So(sc.resp.Code, ShouldEqual, 200)
+				})
+			})
+
+			Convey("Should fail to delete local snapshot when an unexpected 500 error occurs", func() {
+				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						rw.WriteHeader(500)
+						rw.Write([]byte(`{"message":"Unexpected"}`))
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
+					sc.handlerFunc = DeleteDashboardSnapshot
+					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
+
+					So(sc.resp.Code, ShouldEqual, 500)
+				})
+			})
+
+			Convey("Should fail to delete local snapshot when an unexpected remote error occurs", func() {
+				loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/snapshots/12345", "/api/snapshots/:key", m.ROLE_EDITOR, func(sc *scenarioContext) {
+					ts := setupRemoteServer(func(rw http.ResponseWriter, req *http.Request) {
+						rw.WriteHeader(404)
+					})
+
+					mockSnapshotResult.ExternalDeleteUrl = ts.URL
+					sc.handlerFunc = DeleteDashboardSnapshot
+					sc.fakeReqWithParams("DELETE", sc.url, map[string]string{"key": "12345"}).exec()
+
+					So(sc.resp.Code, ShouldEqual, 500)
+				})
+			})
+		})
 	})
 	})
 }
 }

+ 7 - 5
pkg/api/frontendsettings.go

@@ -186,16 +186,18 @@ func getPanelSort(id string) int {
 		sort = 1
 		sort = 1
 	case "singlestat":
 	case "singlestat":
 		sort = 2
 		sort = 2
-	case "table":
+	case "gauge":
 		sort = 3
 		sort = 3
-	case "text":
+	case "table":
 		sort = 4
 		sort = 4
-	case "heatmap":
+	case "text":
 		sort = 5
 		sort = 5
-	case "alertlist":
+	case "heatmap":
 		sort = 6
 		sort = 6
-	case "dashlist":
+	case "alertlist":
 		sort = 7
 		sort = 7
+	case "dashlist":
+		sort = 8
 	}
 	}
 	return sort
 	return sort
 }
 }

+ 8 - 0
pkg/api/plugins.go

@@ -164,6 +164,14 @@ func GetPluginMarkdown(c *m.ReqContext) Response {
 		return Error(500, "Could not get markdown file", err)
 		return Error(500, "Could not get markdown file", err)
 	}
 	}
 
 
+	// fallback try readme
+	if len(content) == 0 {
+		content, err = plugins.GetPluginMarkdown(pluginID, "readme")
+		if err != nil {
+			return Error(501, "Could not get markdown file", err)
+		}
+	}
+
 	resp := Respond(200, content)
 	resp := Respond(200, content)
 	resp.Header("Content-Type", "text/plain; charset=utf-8")
 	resp.Header("Content-Type", "text/plain; charset=utf-8")
 	return resp
 	return resp

+ 1 - 0
pkg/cmd/grafana-server/server.go

@@ -28,6 +28,7 @@ import (
 
 
 	// self registering services
 	// self registering services
 	_ "github.com/grafana/grafana/pkg/extensions"
 	_ "github.com/grafana/grafana/pkg/extensions"
+	_ "github.com/grafana/grafana/pkg/infra/serverlock"
 	_ "github.com/grafana/grafana/pkg/metrics"
 	_ "github.com/grafana/grafana/pkg/metrics"
 	_ "github.com/grafana/grafana/pkg/plugins"
 	_ "github.com/grafana/grafana/pkg/plugins"
 	_ "github.com/grafana/grafana/pkg/services/alerting"
 	_ "github.com/grafana/grafana/pkg/services/alerting"

+ 8 - 0
pkg/infra/serverlock/model.go

@@ -0,0 +1,8 @@
+package serverlock
+
+type serverLock struct {
+	Id            int64
+	OperationUid  string
+	LastExecution int64
+	Version       int64
+}

+ 116 - 0
pkg/infra/serverlock/serverlock.go

@@ -0,0 +1,116 @@
+package serverlock
+
+import (
+	"context"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
+)
+
+func init() {
+	registry.RegisterService(&ServerLockService{})
+}
+
+// ServerLockService allows servers in HA mode to claim a lock
+// and execute an function if the server was granted the lock
+type ServerLockService struct {
+	SQLStore *sqlstore.SqlStore `inject:""`
+	log      log.Logger
+}
+
+// Init this service
+func (sl *ServerLockService) Init() error {
+	sl.log = log.New("infra.lockservice")
+	return nil
+}
+
+// LockAndExecute try to create a lock for this server and only executes the
+// `fn` function when successful. This should not be used at low internal. But services
+// that needs to be run once every ex 10m.
+func (sl *ServerLockService) LockAndExecute(ctx context.Context, actionName string, maxInterval time.Duration, fn func()) error {
+	// gets or creates a lockable row
+	rowLock, err := sl.getOrCreate(ctx, actionName)
+	if err != nil {
+		return err
+	}
+
+	// avoid execution if last lock happened less than `maxInterval` ago
+	if rowLock.LastExecution != 0 {
+		lastExeuctionTime := time.Unix(rowLock.LastExecution, 0)
+		if lastExeuctionTime.Unix() > time.Now().Add(-maxInterval).Unix() {
+			return nil
+		}
+	}
+
+	// try to get lock based on rowLow version
+	acquiredLock, err := sl.acquireLock(ctx, rowLock)
+	if err != nil {
+		return err
+	}
+
+	if acquiredLock {
+		fn()
+	}
+
+	return nil
+}
+
+func (sl *ServerLockService) acquireLock(ctx context.Context, serverLock *serverLock) (bool, error) {
+	var result bool
+
+	err := sl.SQLStore.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
+		newVersion := serverLock.Version + 1
+		sql := `UPDATE server_lock SET
+			version = ?,
+			last_execution = ?
+		WHERE
+			id = ? AND version = ?`
+
+		res, err := dbSession.Exec(sql, newVersion, time.Now().Unix(), serverLock.Id, serverLock.Version)
+		if err != nil {
+			return err
+		}
+
+		affected, err := res.RowsAffected()
+		result = affected == 1
+
+		return err
+	})
+
+	return result, err
+}
+
+func (sl *ServerLockService) getOrCreate(ctx context.Context, actionName string) (*serverLock, error) {
+	var result *serverLock
+
+	err := sl.SQLStore.WithTransactionalDbSession(ctx, func(dbSession *sqlstore.DBSession) error {
+		lockRows := []*serverLock{}
+		err := dbSession.Where("operation_uid = ?", actionName).Find(&lockRows)
+		if err != nil {
+			return err
+		}
+
+		if len(lockRows) > 0 {
+			result = lockRows[0]
+			return nil
+		}
+
+		lockRow := &serverLock{
+			OperationUid:  actionName,
+			LastExecution: 0,
+		}
+
+		_, err = dbSession.Insert(lockRow)
+		if err != nil {
+			return err
+		}
+
+		result = lockRow
+
+		return nil
+	})
+
+	return result, err
+}

+ 40 - 0
pkg/infra/serverlock/serverlock_integration_test.go

@@ -0,0 +1,40 @@
+// +build integration
+
+package serverlock
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestServerLok(t *testing.T) {
+	sl := createTestableServerLock(t)
+
+	Convey("Server lock integration tests", t, func() {
+		counter := 0
+		var err error
+		incCounter := func() { counter++ }
+		atInterval := time.Second * 1
+		ctx := context.Background()
+
+		//this time `fn` should be executed
+		So(sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter), ShouldBeNil)
+
+		//this should not execute `fn`
+		So(sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter), ShouldBeNil)
+		So(sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter), ShouldBeNil)
+		So(sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter), ShouldBeNil)
+		So(sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter), ShouldBeNil)
+
+		// wait 5 second.
+		<-time.After(atInterval * 2)
+
+		// now `fn` should be executed again
+		err = sl.LockAndExecute(ctx, "test-operation", atInterval, incCounter)
+		So(err, ShouldBeNil)
+		So(counter, ShouldEqual, 2)
+	})
+}

+ 55 - 0
pkg/infra/serverlock/serverlock_test.go

@@ -0,0 +1,55 @@
+package serverlock
+
+import (
+	"context"
+	"testing"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func createTestableServerLock(t *testing.T) *ServerLockService {
+	t.Helper()
+
+	sqlstore := sqlstore.InitTestDB(t)
+
+	return &ServerLockService{
+		SQLStore: sqlstore,
+		log:      log.New("test-logger"),
+	}
+}
+
+func TestServerLock(t *testing.T) {
+	Convey("Server lock", t, func() {
+		sl := createTestableServerLock(t)
+		operationUID := "test-operation"
+
+		first, err := sl.getOrCreate(context.Background(), operationUID)
+		So(err, ShouldBeNil)
+
+		lastExecution := first.LastExecution
+		Convey("trying to create three new row locks", func() {
+			for i := 0; i < 3; i++ {
+				first, err = sl.getOrCreate(context.Background(), operationUID)
+				So(err, ShouldBeNil)
+				So(first.OperationUid, ShouldEqual, operationUID)
+				So(first.Id, ShouldEqual, 1)
+			}
+
+			Convey("Should not create new since lock already exist", func() {
+				So(lastExecution, ShouldEqual, first.LastExecution)
+			})
+		})
+
+		Convey("Should be able to create lock on first row", func() {
+			gotLock, err := sl.acquireLock(context.Background(), first)
+			So(err, ShouldBeNil)
+			So(gotLock, ShouldBeTrue)
+
+			gotLock, err = sl.acquireLock(context.Background(), first)
+			So(err, ShouldBeNil)
+			So(gotLock, ShouldBeFalse)
+		})
+	})
+}

+ 1 - 1
pkg/login/ldap.go

@@ -9,11 +9,11 @@ import (
 	"strings"
 	"strings"
 
 
 	"github.com/davecgh/go-spew/spew"
 	"github.com/davecgh/go-spew/spew"
-	"github.com/go-ldap/ldap"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
+	"gopkg.in/ldap.v3"
 )
 )
 
 
 type ILdapConn interface {
 type ILdapConn interface {

+ 1 - 1
pkg/login/ldap_test.go

@@ -5,10 +5,10 @@ import (
 	"crypto/tls"
 	"crypto/tls"
 	"testing"
 	"testing"
 
 
-	"github.com/go-ldap/ldap"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ldap.v3"
 )
 )
 
 
 func TestLdapAuther(t *testing.T) {
 func TestLdapAuther(t *testing.T) {

+ 21 - 7
pkg/middleware/auth_proxy.go

@@ -198,17 +198,31 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error
 	}
 	}
 
 
 	proxies := strings.Split(setting.AuthProxyWhitelist, ",")
 	proxies := strings.Split(setting.AuthProxyWhitelist, ",")
-	sourceIP, _, err := net.SplitHostPort(remoteAddr)
-	if err != nil {
-		return err
+	var proxyObjs []*net.IPNet
+	for _, proxy := range proxies {
+		proxyObjs = append(proxyObjs, coerceProxyAddress(proxy))
 	}
 	}
 
 
-	// Compare allowed IP addresses to actual address
-	for _, proxyIP := range proxies {
-		if sourceIP == strings.TrimSpace(proxyIP) {
+	sourceIP, _, _ := net.SplitHostPort(remoteAddr)
+	sourceObj := net.ParseIP(sourceIP)
+
+	for _, proxyObj := range proxyObjs {
+		if proxyObj.Contains(sourceObj) {
 			return nil
 			return nil
 		}
 		}
 	}
 	}
-
 	return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
 	return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP)
 }
 }
+
+func coerceProxyAddress(proxyAddr string) *net.IPNet {
+	proxyAddr = strings.TrimSpace(proxyAddr)
+	if !strings.Contains(proxyAddr, "/") {
+		proxyAddr = strings.Join([]string{proxyAddr, "32"}, "/")
+	}
+
+	_, network, err := net.ParseCIDR(proxyAddr)
+	if err != nil {
+		fmt.Println(err)
+	}
+	return network
+}

+ 90 - 0
pkg/middleware/middleware_test.go

@@ -271,6 +271,23 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is not within trusted CIDR block", func(sc *scenarioContext) {
+			setting.AuthProxyEnabled = true
+			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
+			setting.AuthProxyHeaderProperty = "username"
+			setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
+
+			sc.fakeReq("GET", "/")
+			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
+			sc.req.RemoteAddr = "192.168.3.1:12345"
+			sc.exec()
+
+			Convey("should return 407 status code", func() {
+				So(sc.resp.Code, ShouldEqual, 407)
+				So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 192.168.3.1 is not from the authentication proxy")
+			})
+		})
+
 		middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) {
 		middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not trusted", func(sc *scenarioContext) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@@ -288,6 +305,23 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is not within trusted CIDR block", func(sc *scenarioContext) {
+			setting.AuthProxyEnabled = true
+			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
+			setting.AuthProxyHeaderProperty = "username"
+			setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
+
+			sc.fakeReq("GET", "/")
+			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
+			sc.req.RemoteAddr = "[2001:23]:12345"
+			sc.exec()
+
+			Convey("should return 407 status code", func() {
+				So(sc.resp.Code, ShouldEqual, 407)
+				So(sc.resp.Body.String(), ShouldContainSubstring, "Request for user (torkelo) from 2001:23 is not from the authentication proxy")
+			})
+		})
+
 		middlewareScenario("When auth_proxy is enabled and request RemoteAddr is trusted", func(sc *scenarioContext) {
 		middlewareScenario("When auth_proxy is enabled and request RemoteAddr is trusted", func(sc *scenarioContext) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
@@ -316,6 +350,62 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 			})
 		})
 		})
 
 
+		middlewareScenario("When auth_proxy is enabled and IPv4 request RemoteAddr is within trusted CIDR block", func(sc *scenarioContext) {
+			setting.AuthProxyEnabled = true
+			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
+			setting.AuthProxyHeaderProperty = "username"
+			setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
+
+			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
+				query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
+				return nil
+			})
+
+			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+				cmd.Result = &m.User{Id: 33}
+				return nil
+			})
+
+			sc.fakeReq("GET", "/")
+			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
+			sc.req.RemoteAddr = "192.168.1.10:12345"
+			sc.exec()
+
+			Convey("Should init context with user info", func() {
+				So(sc.context.IsSignedIn, ShouldBeTrue)
+				So(sc.context.UserId, ShouldEqual, 33)
+				So(sc.context.OrgId, ShouldEqual, 4)
+			})
+		})
+
+		middlewareScenario("When auth_proxy is enabled and IPv6 request RemoteAddr is within trusted CIDR block", func(sc *scenarioContext) {
+			setting.AuthProxyEnabled = true
+			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
+			setting.AuthProxyHeaderProperty = "username"
+			setting.AuthProxyWhitelist = "192.168.1.0/24, 2001::0/120"
+
+			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
+				query.Result = &m.SignedInUser{OrgId: 4, UserId: 33}
+				return nil
+			})
+
+			bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error {
+				cmd.Result = &m.User{Id: 33}
+				return nil
+			})
+
+			sc.fakeReq("GET", "/")
+			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
+			sc.req.RemoteAddr = "[2001::23]:12345"
+			sc.exec()
+
+			Convey("Should init context with user info", func() {
+				So(sc.context.IsSignedIn, ShouldBeTrue)
+				So(sc.context.UserId, ShouldEqual, 33)
+				So(sc.context.OrgId, ShouldEqual, 4)
+			})
+		})
+
 		middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
 		middlewareScenario("When session exists for previous user, create a new session", func(sc *scenarioContext) {
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyEnabled = true
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"
 			setting.AuthProxyHeaderName = "X-WEBAUTH-USER"

+ 13 - 9
pkg/models/dashboard_snapshot.go

@@ -8,14 +8,15 @@ import (
 
 
 // DashboardSnapshot model
 // DashboardSnapshot model
 type DashboardSnapshot struct {
 type DashboardSnapshot struct {
-	Id          int64
-	Name        string
-	Key         string
-	DeleteKey   string
-	OrgId       int64
-	UserId      int64
-	External    bool
-	ExternalUrl string
+	Id                int64
+	Name              string
+	Key               string
+	DeleteKey         string
+	OrgId             int64
+	UserId            int64
+	External          bool
+	ExternalUrl       string
+	ExternalDeleteUrl string
 
 
 	Expires time.Time
 	Expires time.Time
 	Created time.Time
 	Created time.Time
@@ -48,7 +49,10 @@ type CreateDashboardSnapshotCommand struct {
 	Expires   int64            `json:"expires"`
 	Expires   int64            `json:"expires"`
 
 
 	// these are passed when storing an external snapshot ref
 	// these are passed when storing an external snapshot ref
-	External  bool   `json:"external"`
+	External          bool   `json:"external"`
+	ExternalUrl       string `json:"-"`
+	ExternalDeleteUrl string `json:"-"`
+
 	Key       string `json:"key"`
 	Key       string `json:"key"`
 	DeleteKey string `json:"deleteKey"`
 	DeleteKey string `json:"deleteKey"`
 
 

+ 1 - 12
pkg/plugins/datasource_plugin.go

@@ -3,10 +3,8 @@ package plugins
 import (
 import (
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
-	"os"
 	"os/exec"
 	"os/exec"
 	"path"
 	"path"
-	"path/filepath"
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana-plugin-model/go/datasource"
 	"github.com/grafana/grafana-plugin-model/go/datasource"
@@ -24,11 +22,11 @@ type DataSourcePlugin struct {
 	Metrics      bool              `json:"metrics"`
 	Metrics      bool              `json:"metrics"`
 	Alerting     bool              `json:"alerting"`
 	Alerting     bool              `json:"alerting"`
 	Explore      bool              `json:"explore"`
 	Explore      bool              `json:"explore"`
+	Table        bool              `json:"tables"`
 	Logs         bool              `json:"logs"`
 	Logs         bool              `json:"logs"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	QueryOptions map[string]bool   `json:"queryOptions,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	BuiltIn      bool              `json:"builtIn,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
 	Mixed        bool              `json:"mixed,omitempty"`
-	HasQueryHelp bool              `json:"hasQueryHelp,omitempty"`
 	Routes       []*AppPluginRoute `json:"routes"`
 	Routes       []*AppPluginRoute `json:"routes"`
 
 
 	Backend    bool   `json:"backend,omitempty"`
 	Backend    bool   `json:"backend,omitempty"`
@@ -47,15 +45,6 @@ func (p *DataSourcePlugin) Load(decoder *json.Decoder, pluginDir string) error {
 		return err
 		return err
 	}
 	}
 
 
-	// look for help markdown
-	helpPath := filepath.Join(p.PluginDir, "QUERY_HELP.md")
-	if _, err := os.Stat(helpPath); os.IsNotExist(err) {
-		helpPath = filepath.Join(p.PluginDir, "query_help.md")
-	}
-	if _, err := os.Stat(helpPath); err == nil {
-		p.HasQueryHelp = true
-	}
-
 	DataSources[p.Id] = p
 	DataSources[p.Id] = p
 	return nil
 	return nil
 }
 }

+ 4 - 2
pkg/services/alerting/notifier.go

@@ -166,7 +166,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
 
 
 	var result notifierStateSlice
 	var result notifierStateSlice
 	for _, notification := range query.Result {
 	for _, notification := range query.Result {
-		not, err := n.createNotifierFor(notification)
+		not, err := InitNotifier(notification)
 		if err != nil {
 		if err != nil {
 			n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
 			n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
 			continue
 			continue
@@ -195,7 +195,8 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
 	return result, nil
 	return result, nil
 }
 }
 
 
-func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
+// InitNotifier instantiate a new notifier based on the model
+func InitNotifier(model *m.AlertNotification) (Notifier, error) {
 	notifierPlugin, found := notifierFactories[model.Type]
 	notifierPlugin, found := notifierFactories[model.Type]
 	if !found {
 	if !found {
 		return nil, errors.New("Unsupported notification type")
 		return nil, errors.New("Unsupported notification type")
@@ -208,6 +209,7 @@ type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
 
 
 var notifierFactories = make(map[string]*NotifierPlugin)
 var notifierFactories = make(map[string]*NotifierPlugin)
 
 
+// RegisterNotifier register an notifier
 func RegisterNotifier(plugin *NotifierPlugin) {
 func RegisterNotifier(plugin *NotifierPlugin) {
 	notifierFactories[plugin.Type] = plugin
 	notifierFactories[plugin.Type] = plugin
 }
 }

+ 1 - 1
pkg/services/alerting/test_notification.go

@@ -32,7 +32,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
 		Settings: cmd.Settings,
 		Settings: cmd.Settings,
 	}
 	}
 
 
-	notifiers, err := notifier.createNotifierFor(model)
+	notifiers, err := InitNotifier(model)
 
 
 	if err != nil {
 	if err != nil {
 		log.Error2("Failed to create notifier", "error", err.Error())
 		log.Error2("Failed to create notifier", "error", err.Error())

+ 8 - 3
pkg/services/cleanup/cleanup.go

@@ -8,6 +8,7 @@ import (
 	"time"
 	"time"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
+	"github.com/grafana/grafana/pkg/infra/serverlock"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/registry"
@@ -15,8 +16,9 @@ import (
 )
 )
 
 
 type CleanUpService struct {
 type CleanUpService struct {
-	log log.Logger
-	Cfg *setting.Cfg `inject:""`
+	log               log.Logger
+	Cfg               *setting.Cfg                  `inject:""`
+	ServerLockService *serverlock.ServerLockService `inject:""`
 }
 }
 
 
 func init() {
 func init() {
@@ -38,7 +40,10 @@ func (srv *CleanUpService) Run(ctx context.Context) error {
 			srv.cleanUpTmpFiles()
 			srv.cleanUpTmpFiles()
 			srv.deleteExpiredSnapshots()
 			srv.deleteExpiredSnapshots()
 			srv.deleteExpiredDashboardVersions()
 			srv.deleteExpiredDashboardVersions()
-			srv.deleteOldLoginAttempts()
+			srv.ServerLockService.LockAndExecute(ctx, "delete old login attempts", time.Minute*10, func() {
+				srv.deleteOldLoginAttempts()
+			})
+
 		case <-ctx.Done():
 		case <-ctx.Done():
 			return ctx.Err()
 			return ctx.Err()
 		}
 		}

+ 12 - 10
pkg/services/sqlstore/dashboard_snapshot.go

@@ -47,16 +47,18 @@ func CreateDashboardSnapshot(cmd *m.CreateDashboardSnapshotCommand) error {
 		}
 		}
 
 
 		snapshot := &m.DashboardSnapshot{
 		snapshot := &m.DashboardSnapshot{
-			Name:      cmd.Name,
-			Key:       cmd.Key,
-			DeleteKey: cmd.DeleteKey,
-			OrgId:     cmd.OrgId,
-			UserId:    cmd.UserId,
-			External:  cmd.External,
-			Dashboard: cmd.Dashboard,
-			Expires:   expires,
-			Created:   time.Now(),
-			Updated:   time.Now(),
+			Name:              cmd.Name,
+			Key:               cmd.Key,
+			DeleteKey:         cmd.DeleteKey,
+			OrgId:             cmd.OrgId,
+			UserId:            cmd.UserId,
+			External:          cmd.External,
+			ExternalUrl:       cmd.ExternalUrl,
+			ExternalDeleteUrl: cmd.ExternalDeleteUrl,
+			Dashboard:         cmd.Dashboard,
+			Expires:           expires,
+			Created:           time.Now(),
+			Updated:           time.Now(),
 		}
 		}
 
 
 		_, err := sess.Insert(snapshot)
 		_, err := sess.Insert(snapshot)

+ 2 - 2
pkg/services/sqlstore/datasource.go

@@ -53,14 +53,14 @@ func GetDataSourceByName(query *m.GetDataSourceByNameQuery) error {
 }
 }
 
 
 func GetDataSources(query *m.GetDataSourcesQuery) error {
 func GetDataSources(query *m.GetDataSourcesQuery) error {
-	sess := x.Limit(1000, 0).Where("org_id=?", query.OrgId).Asc("name")
+	sess := x.Limit(5000, 0).Where("org_id=?", query.OrgId).Asc("name")
 
 
 	query.Result = make([]*m.DataSource, 0)
 	query.Result = make([]*m.DataSource, 0)
 	return sess.Find(&query.Result)
 	return sess.Find(&query.Result)
 }
 }
 
 
 func GetAllDataSources(query *m.GetAllDataSourcesQuery) error {
 func GetAllDataSources(query *m.GetAllDataSourcesQuery) error {
-	sess := x.Limit(1000, 0).Asc("name")
+	sess := x.Limit(5000, 0).Asc("name")
 
 
 	query.Result = make([]*m.DataSource, 0)
 	query.Result = make([]*m.DataSource, 0)
 	return sess.Find(&query.Result)
 	return sess.Find(&query.Result)

+ 4 - 0
pkg/services/sqlstore/migrations/dashboard_snapshot_mig.go

@@ -60,4 +60,8 @@ func addDashboardSnapshotMigrations(mg *Migrator) {
 		{Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false},
 		{Name: "external_url", Type: DB_NVarchar, Length: 255, Nullable: false},
 		{Name: "dashboard", Type: DB_MediumText, Nullable: false},
 		{Name: "dashboard", Type: DB_MediumText, Nullable: false},
 	}))
 	}))
+
+	mg.AddMigration("Add column external_delete_url to dashboard_snapshots table", NewAddColumnMigration(snapshotV5, &Column{
+		Name: "external_delete_url", Type: DB_NVarchar, Length: 255, Nullable: true,
+	}))
 }
 }

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

@@ -31,6 +31,7 @@ func AddMigrations(mg *Migrator) {
 	addTagMigration(mg)
 	addTagMigration(mg)
 	addLoginAttemptMigrations(mg)
 	addLoginAttemptMigrations(mg)
 	addUserAuthMigrations(mg)
 	addUserAuthMigrations(mg)
+	addServerlockMigrations(mg)
 }
 }
 
 
 func addMigrationLogMigrations(mg *Migrator) {
 func addMigrationLogMigrations(mg *Migrator) {

+ 22 - 0
pkg/services/sqlstore/migrations/serverlock_migrations.go

@@ -0,0 +1,22 @@
+package migrations
+
+import "github.com/grafana/grafana/pkg/services/sqlstore/migrator"
+
+func addServerlockMigrations(mg *migrator.Migrator) {
+	serverLock := migrator.Table{
+		Name: "server_lock",
+		Columns: []*migrator.Column{
+			{Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true},
+			{Name: "operation_uid", Type: migrator.DB_NVarchar, Length: 100},
+			{Name: "version", Type: migrator.DB_BigInt},
+			{Name: "last_execution", Type: migrator.DB_BigInt, Nullable: false},
+		},
+		Indices: []*migrator.Index{
+			{Cols: []string{"operation_uid"}, Type: migrator.UniqueIndex},
+		},
+	}
+
+	mg.AddMigration("create server_lock table", migrator.NewAddTableMigration(serverLock))
+
+	mg.AddMigration("add index server_lock.operation_uid", migrator.NewAddIndexMigration(serverLock, serverLock.Indices[0]))
+}

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

@@ -345,8 +345,12 @@ func GetUserOrgList(query *m.GetUserOrgListQuery) error {
 	return err
 	return err
 }
 }
 
 
+func newSignedInUserCacheKey(orgID, userID int64) string {
+	return fmt.Sprintf("signed-in-user-%d-%d", userID, orgID)
+}
+
 func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
 func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) error {
-	cacheKey := fmt.Sprintf("signed-in-user-%d-%d", query.UserId, query.OrgId)
+	cacheKey := newSignedInUserCacheKey(query.OrgId, query.UserId)
 	if cached, found := ss.CacheService.Get(cacheKey); found {
 	if cached, found := ss.CacheService.Get(cacheKey); found {
 		query.Result = cached.(*m.SignedInUser)
 		query.Result = cached.(*m.SignedInUser)
 		return nil
 		return nil
@@ -357,6 +361,7 @@ func (ss *SqlStore) GetSignedInUserWithCache(query *m.GetSignedInUserQuery) erro
 		return err
 		return err
 	}
 	}
 
 
+	cacheKey = newSignedInUserCacheKey(query.Result.OrgId, query.UserId)
 	ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
 	ss.CacheService.Set(cacheKey, query.Result, time.Second*5)
 	return nil
 	return nil
 }
 }

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

@@ -13,7 +13,7 @@ import (
 func TestUserDataAccess(t *testing.T) {
 func TestUserDataAccess(t *testing.T) {
 
 
 	Convey("Testing DB", t, func() {
 	Convey("Testing DB", t, func() {
-		InitTestDB(t)
+		ss := InitTestDB(t)
 
 
 		Convey("Creating a user", func() {
 		Convey("Creating a user", func() {
 			cmd := &m.CreateUserCommand{
 			cmd := &m.CreateUserCommand{
@@ -153,6 +153,27 @@ func TestUserDataAccess(t *testing.T) {
 						So(prefsQuery.Result.UserId, ShouldEqual, 0)
 						So(prefsQuery.Result.UserId, ShouldEqual, 0)
 					})
 					})
 				})
 				})
+
+				Convey("when retreiving signed in user for orgId=0 result should return active org id", func() {
+					ss.CacheService.Flush()
+
+					query := &m.GetSignedInUserQuery{OrgId: users[1].OrgId, UserId: users[1].Id}
+					err := ss.GetSignedInUserWithCache(query)
+					So(err, ShouldBeNil)
+					So(query.Result, ShouldNotBeNil)
+					So(query.OrgId, ShouldEqual, users[1].OrgId)
+					err = SetUsingOrg(&m.SetUsingOrgCommand{UserId: users[1].Id, OrgId: users[0].OrgId})
+					So(err, ShouldBeNil)
+					query = &m.GetSignedInUserQuery{OrgId: 0, UserId: users[1].Id}
+					err = ss.GetSignedInUserWithCache(query)
+					So(err, ShouldBeNil)
+					So(query.Result, ShouldNotBeNil)
+					So(query.Result.OrgId, ShouldEqual, users[0].OrgId)
+
+					cacheKey := newSignedInUserCacheKey(query.Result.OrgId, query.UserId)
+					_, found := ss.CacheService.Get(cacheKey)
+					So(found, ShouldBeTrue)
+				})
 			})
 			})
 		})
 		})
 
 

+ 15 - 14
pkg/setting/setting_oauth.go

@@ -1,20 +1,21 @@
 package setting
 package setting
 
 
 type OAuthInfo struct {
 type OAuthInfo struct {
-	ClientId, ClientSecret string
-	Scopes                 []string
-	AuthUrl, TokenUrl      string
-	Enabled                bool
-	EmailAttributeName     string
-	AllowedDomains         []string
-	HostedDomain           string
-	ApiUrl                 string
-	AllowSignup            bool
-	Name                   string
-	TlsClientCert          string
-	TlsClientKey           string
-	TlsClientCa            string
-	TlsSkipVerify          bool
+	ClientId, ClientSecret       string
+	Scopes                       []string
+	AuthUrl, TokenUrl            string
+	Enabled                      bool
+	EmailAttributeName           string
+	AllowedDomains               []string
+	HostedDomain                 string
+	ApiUrl                       string
+	AllowSignup                  bool
+	Name                         string
+	TlsClientCert                string
+	TlsClientKey                 string
+	TlsClientCa                  string
+	TlsSkipVerify                bool
+	SendClientCredentialsViaPost bool
 }
 }
 
 
 type OAuther struct {
 type OAuther struct {

+ 22 - 16
pkg/social/social.go

@@ -63,28 +63,34 @@ func NewOAuthService() {
 	for _, name := range allOauthes {
 	for _, name := range allOauthes {
 		sec := setting.Raw.Section("auth." + name)
 		sec := setting.Raw.Section("auth." + name)
 		info := &setting.OAuthInfo{
 		info := &setting.OAuthInfo{
-			ClientId:           sec.Key("client_id").String(),
-			ClientSecret:       sec.Key("client_secret").String(),
-			Scopes:             util.SplitString(sec.Key("scopes").String()),
-			AuthUrl:            sec.Key("auth_url").String(),
-			TokenUrl:           sec.Key("token_url").String(),
-			ApiUrl:             sec.Key("api_url").String(),
-			Enabled:            sec.Key("enabled").MustBool(),
-			EmailAttributeName: sec.Key("email_attribute_name").String(),
-			AllowedDomains:     util.SplitString(sec.Key("allowed_domains").String()),
-			HostedDomain:       sec.Key("hosted_domain").String(),
-			AllowSignup:        sec.Key("allow_sign_up").MustBool(),
-			Name:               sec.Key("name").MustString(name),
-			TlsClientCert:      sec.Key("tls_client_cert").String(),
-			TlsClientKey:       sec.Key("tls_client_key").String(),
-			TlsClientCa:        sec.Key("tls_client_ca").String(),
-			TlsSkipVerify:      sec.Key("tls_skip_verify_insecure").MustBool(),
+			ClientId:                     sec.Key("client_id").String(),
+			ClientSecret:                 sec.Key("client_secret").String(),
+			Scopes:                       util.SplitString(sec.Key("scopes").String()),
+			AuthUrl:                      sec.Key("auth_url").String(),
+			TokenUrl:                     sec.Key("token_url").String(),
+			ApiUrl:                       sec.Key("api_url").String(),
+			Enabled:                      sec.Key("enabled").MustBool(),
+			EmailAttributeName:           sec.Key("email_attribute_name").String(),
+			AllowedDomains:               util.SplitString(sec.Key("allowed_domains").String()),
+			HostedDomain:                 sec.Key("hosted_domain").String(),
+			AllowSignup:                  sec.Key("allow_sign_up").MustBool(),
+			Name:                         sec.Key("name").MustString(name),
+			TlsClientCert:                sec.Key("tls_client_cert").String(),
+			TlsClientKey:                 sec.Key("tls_client_key").String(),
+			TlsClientCa:                  sec.Key("tls_client_ca").String(),
+			TlsSkipVerify:                sec.Key("tls_skip_verify_insecure").MustBool(),
+			SendClientCredentialsViaPost: sec.Key("send_client_credentials_via_post").MustBool(),
 		}
 		}
 
 
 		if !info.Enabled {
 		if !info.Enabled {
 			continue
 			continue
 		}
 		}
 
 
+		// handle the clients that do not properly support Basic auth headers and require passing client_id/client_secret via POST payload
+		if info.SendClientCredentialsViaPost {
+			oauth2.RegisterBrokenAuthHeaderProvider(info.TokenUrl)
+		}
+
 		if name == "grafananet" {
 		if name == "grafananet" {
 			name = grafanaCom
 			name = grafanaCom
 		}
 		}

+ 38 - 0
public/app/core/components/Animations/FadeIn.tsx

@@ -0,0 +1,38 @@
+import React, { SFC } from 'react';
+import Transition from 'react-transition-group/Transition';
+
+interface Props {
+  duration: number;
+  children: JSX.Element;
+  in: boolean;
+  unmountOnExit?: boolean;
+}
+
+export const FadeIn: SFC<Props> = props => {
+  const defaultStyle = {
+    transition: `opacity ${props.duration}ms linear`,
+    opacity: 0,
+  };
+
+  const transitionStyles = {
+    exited: { opacity: 0, display: 'none' },
+    entering: { opacity: 0 },
+    entered: { opacity: 1 },
+    exiting: { opacity: 0 },
+  };
+
+  return (
+    <Transition in={props.in} timeout={props.duration} unmountOnExit={props.unmountOnExit || false}>
+      {state => (
+        <div
+          style={{
+            ...defaultStyle,
+            ...transitionStyles[state],
+          }}
+        >
+          {props.children}
+        </div>
+      )}
+    </Transition>
+  );
+};

+ 1 - 1
public/app/core/components/Animations/SlideDown.tsx

@@ -23,7 +23,7 @@ export default ({ children, in: inProp, maxHeight = defaultMaxHeight, style = de
   const transitionStyles = {
   const transitionStyles = {
     exited: { maxHeight: 0 },
     exited: { maxHeight: 0 },
     entering: { maxHeight: maxHeight },
     entering: { maxHeight: maxHeight },
-    entered: { maxHeight: maxHeight, overflow: 'visible' },
+    entered: { maxHeight: 'unset', overflow: 'visible' },
     exiting: { maxHeight: 0 },
     exiting: { maxHeight: 0 },
   };
   };
 
 

+ 1 - 0
public/app/core/components/AppNotifications/AppNotificationItem.tsx

@@ -20,6 +20,7 @@ export default class AppNotificationItem extends Component<Props> {
 
 
   render() {
   render() {
     const { appNotification, onClearNotification } = this.props;
     const { appNotification, onClearNotification } = this.props;
+
     return (
     return (
       <div className={`alert-${appNotification.severity} alert`}>
       <div className={`alert-${appNotification.severity} alert`}>
         <div className="alert-icon">
         <div className="alert-icon">

+ 36 - 0
public/app/core/components/ClickOutsideWrapper/ClickOutsideWrapper.tsx

@@ -0,0 +1,36 @@
+import { PureComponent } from 'react';
+import ReactDOM from 'react-dom';
+
+export interface Props {
+  onClick: () => void;
+}
+
+interface State {
+  hasEventListener: boolean;
+}
+
+export class ClickOutsideWrapper extends PureComponent<Props, State> {
+  state = {
+    hasEventListener: false,
+  };
+
+  componentDidMount() {
+    window.addEventListener('click', this.onOutsideClick, false);
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener('click', this.onOutsideClick, false);
+  }
+
+  onOutsideClick = event => {
+    const domNode = ReactDOM.findDOMNode(this) as Element;
+
+    if (!domNode || !domNode.contains(event.target)) {
+      this.props.onClick();
+    }
+  };
+
+  render() {
+    return this.props.children;
+  }
+}

+ 67 - 0
public/app/core/components/CopyToClipboard/CopyToClipboard.tsx

@@ -0,0 +1,67 @@
+import React, { PureComponent, ReactNode } from 'react';
+import ClipboardJS from 'clipboard';
+
+interface Props {
+  text: () => string;
+  elType?: string;
+  onSuccess?: (evt: any) => void;
+  onError?: (evt: any) => void;
+  className?: string;
+  children?: ReactNode;
+}
+
+export class CopyToClipboard extends PureComponent<Props> {
+  clipboardjs: any;
+  myRef: any;
+
+  constructor(props) {
+    super(props);
+    this.myRef = React.createRef();
+  }
+
+  componentDidMount() {
+    const { text, onSuccess, onError } = this.props;
+
+    this.clipboardjs = new ClipboardJS(this.myRef.current, {
+      text: text,
+    });
+
+    if (onSuccess) {
+      this.clipboardjs.on('success', evt => {
+        evt.clearSelection();
+        onSuccess(evt);
+      });
+    }
+
+    if (onError) {
+      this.clipboardjs.on('error', evt => {
+        console.error('Action:', evt.action);
+        console.error('Trigger:', evt.trigger);
+        onError(evt);
+      });
+    }
+  }
+
+  componentWillUnmount() {
+    if (this.clipboardjs) {
+      this.clipboardjs.destroy();
+    }
+  }
+
+  getElementType = () => {
+    return this.props.elType || 'button';
+  };
+
+  render() {
+    const { elType, text, children, onError, onSuccess, ...restProps } = this.props;
+
+    return React.createElement(
+      this.getElementType(),
+      {
+        ref: this.myRef,
+        ...restProps,
+      },
+      this.props.children
+    );
+  }
+}

+ 2 - 2
public/app/core/components/CustomScrollbar/CustomScrollbar.tsx

@@ -28,8 +28,8 @@ class CustomScrollbar extends PureComponent<Props> {
       <Scrollbars
       <Scrollbars
         className={customClassName}
         className={customClassName}
         autoHeight={true}
         autoHeight={true}
-        autoHeightMin={'100%'}
-        autoHeightMax={'100%'}
+        autoHeightMin={'inherit'}
+        autoHeightMax={'inherit'}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderTrackVertical={props => <div {...props} className="track-vertical" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}

+ 4 - 4
public/app/core/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

@@ -6,8 +6,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
   style={
   style={
     Object {
     Object {
       "height": "auto",
       "height": "auto",
-      "maxHeight": "100%",
-      "minHeight": "100%",
+      "maxHeight": "inherit",
+      "minHeight": "inherit",
       "overflow": "hidden",
       "overflow": "hidden",
       "position": "relative",
       "position": "relative",
       "width": "100%",
       "width": "100%",
@@ -23,8 +23,8 @@ exports[`CustomScrollbar renders correctly 1`] = `
         "left": undefined,
         "left": undefined,
         "marginBottom": 0,
         "marginBottom": 0,
         "marginRight": 0,
         "marginRight": 0,
-        "maxHeight": "calc(100% + 0px)",
-        "minHeight": "calc(100% + 0px)",
+        "maxHeight": "calc(inherit + 0px)",
+        "minHeight": "calc(inherit + 0px)",
         "overflow": "scroll",
         "overflow": "scroll",
         "position": "relative",
         "position": "relative",
         "right": undefined,
         "right": undefined,

+ 43 - 0
public/app/core/components/Form/Element.tsx

@@ -0,0 +1,43 @@
+import React, { PureComponent, ReactNode, ReactElement } from 'react';
+import { Label } from './Label';
+import { uniqueId } from 'lodash';
+
+interface Props {
+  label?: ReactNode;
+  labelClassName?: string;
+  id?: string;
+  children: ReactElement<any>;
+}
+
+export class Element extends PureComponent<Props> {
+  elementId: string = this.props.id || uniqueId('form-element-');
+
+  get elementLabel() {
+    const { label, labelClassName } = this.props;
+
+    if (label) {
+      return (
+        <Label htmlFor={this.elementId} className={labelClassName}>
+          {label}
+        </Label>
+      );
+    }
+
+    return null;
+  }
+
+  get children() {
+    const { children } = this.props;
+
+    return React.cloneElement(children, { id: this.elementId });
+  }
+
+  render() {
+    return (
+      <div className="our-custom-wrapper-class">
+        {this.elementLabel}
+        {this.children}
+      </div>
+    );
+  }
+}

+ 53 - 0
public/app/core/components/Form/Input.test.tsx

@@ -0,0 +1,53 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { shallow } from 'enzyme';
+import { Input, EventsWithValidation } from './Input';
+import { ValidationEvents } from 'app/types';
+
+const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars';
+const testBlurValidation: ValidationEvents = {
+  [EventsWithValidation.onBlur]: [
+    {
+      rule: (value: string) => {
+        if (!value || value.length < 3) {
+          return true;
+        }
+        return false;
+      },
+      errorMessage: TEST_ERROR_MESSAGE,
+    },
+  ],
+};
+
+describe('Input', () => {
+  it('renders correctly', () => {
+    const tree = renderer.create(<Input />).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+
+  it('should validate with error onBlur', () => {
+    const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
+    const evt = {
+      persist: jest.fn,
+      target: {
+        value: 'I can not be more than 2 chars',
+      },
+    };
+
+    wrapper.find('input').simulate('blur', evt);
+    expect(wrapper.state('error')).toBe(TEST_ERROR_MESSAGE);
+  });
+
+  it('should validate without error onBlur', () => {
+    const wrapper = shallow(<Input validationEvents={testBlurValidation} />);
+    const evt = {
+      persist: jest.fn,
+      target: {
+        value: 'Hi',
+      },
+    };
+
+    wrapper.find('input').simulate('blur', evt);
+    expect(wrapper.state('error')).toBe(null);
+  });
+});

+ 94 - 0
public/app/core/components/Form/Input.tsx

@@ -0,0 +1,94 @@
+import React, { PureComponent } from 'react';
+import classNames from 'classnames';
+import { ValidationEvents, ValidationRule } from 'app/types';
+import { validate, hasValidationEvent } from 'app/core/utils/validate';
+
+export enum InputStatus {
+  Invalid = 'invalid',
+  Valid = 'valid',
+}
+
+export enum InputTypes {
+  Text = 'text',
+  Number = 'number',
+  Password = 'password',
+  Email = 'email',
+}
+
+export enum EventsWithValidation {
+  onBlur = 'onBlur',
+  onFocus = 'onFocus',
+  onChange = 'onChange',
+}
+
+interface Props extends React.HTMLProps<HTMLInputElement> {
+  validationEvents?: ValidationEvents;
+  hideErrorMessage?: boolean;
+
+  // Override event props and append status as argument
+  onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
+  onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: InputStatus) => void;
+  onChange?: (event: React.FormEvent<HTMLInputElement>, status?: InputStatus) => void;
+}
+
+export class Input extends PureComponent<Props> {
+  static defaultProps = {
+    className: '',
+  };
+
+  state = {
+    error: null,
+  };
+
+  get status() {
+    return this.state.error ? InputStatus.Invalid : InputStatus.Valid;
+  }
+
+  get isInvalid() {
+    return this.status === InputStatus.Invalid;
+  }
+
+  validatorAsync = (validationRules: ValidationRule[]) => {
+    return evt => {
+      const errors = validate(evt.target.value, validationRules);
+      this.setState(prevState => {
+        return {
+          ...prevState,
+          error: errors ? errors[0] : null,
+        };
+      });
+    };
+  };
+
+  populateEventPropsWithStatus = (restProps, validationEvents: ValidationEvents) => {
+    const inputElementProps = { ...restProps };
+    Object.keys(EventsWithValidation).forEach((eventName: EventsWithValidation) => {
+      if (hasValidationEvent(eventName, validationEvents) || restProps[eventName]) {
+        inputElementProps[eventName] = async evt => {
+          evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
+          if (hasValidationEvent(eventName, validationEvents)) {
+            await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
+          }
+          if (restProps[eventName]) {
+            restProps[eventName].apply(null, [evt, this.status]);
+          }
+        };
+      }
+    });
+    return inputElementProps;
+  };
+
+  render() {
+    const { validationEvents, className, hideErrorMessage, ...restProps } = this.props;
+    const { error } = this.state;
+    const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
+    const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
+
+    return (
+      <div className="our-custom-wrapper-class">
+        <input {...inputElementProps} className={inputClassName} />
+        {error && !hideErrorMessage && <span>{error}</span>}
+      </div>
+    );
+  }
+}

+ 19 - 0
public/app/core/components/Form/Label.tsx

@@ -0,0 +1,19 @@
+import React, { PureComponent, ReactNode } from 'react';
+
+interface Props {
+  children: ReactNode;
+  htmlFor?: string;
+  className?: string;
+}
+
+export class Label extends PureComponent<Props> {
+  render() {
+    const { children, htmlFor, className } = this.props;
+
+    return (
+      <label className={`custom-label-class ${className || ''}`} htmlFor={htmlFor}>
+        {children}
+      </label>
+    );
+  }
+}

+ 11 - 0
public/app/core/components/Form/__snapshots__/Input.test.tsx.snap

@@ -0,0 +1,11 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Input renders correctly 1`] = `
+<div
+  className="our-custom-wrapper-class"
+>
+  <input
+    className="gf-form-input"
+  />
+</div>
+`;

+ 3 - 0
public/app/core/components/Form/index.ts

@@ -0,0 +1,3 @@
+export { Element } from './Element';
+export { Input } from './Input';
+export { Label } from './Label';

+ 51 - 0
public/app/core/components/JSONFormatter/JSONFormatter.tsx

@@ -0,0 +1,51 @@
+import React, { PureComponent, createRef } from 'react';
+// import JSONFormatterJS, { JSONFormatterConfiguration } from 'json-formatter-js';
+import { JsonExplorer } from 'app/core/core'; // We have made some monkey-patching of json-formatter-js so we can't switch right now
+
+interface Props {
+  className?: string;
+  json: {};
+  config?: any;
+  open?: number;
+  onDidRender?: (formattedJson: any) => void;
+}
+
+export class JSONFormatter extends PureComponent<Props> {
+  private wrapperRef = createRef<HTMLDivElement>();
+
+  static defaultProps = {
+    open: 3,
+    config: {
+      animateOpen: true,
+    },
+  };
+
+  componentDidMount() {
+    this.renderJson();
+  }
+
+  componentDidUpdate() {
+    this.renderJson();
+  }
+
+  renderJson = () => {
+    const { json, config, open, onDidRender } = this.props;
+    const wrapperEl = this.wrapperRef.current;
+    const formatter = new JsonExplorer(json, open, config);
+    const hasChildren: boolean = wrapperEl.hasChildNodes();
+    if (hasChildren) {
+      wrapperEl.replaceChild(formatter.render(), wrapperEl.lastChild);
+    } else {
+      wrapperEl.appendChild(formatter.render());
+    }
+
+    if (onDidRender) {
+      onDidRender(formatter.json);
+    }
+  };
+
+  render() {
+    const { className } = this.props;
+    return <div className={className} ref={this.wrapperRef} />;
+  }
+}

+ 1 - 0
public/app/core/components/Label/Label.tsx

@@ -6,6 +6,7 @@ interface Props {
   for?: string;
   for?: string;
   children: ReactNode;
   children: ReactNode;
   width?: number;
   width?: number;
+  className?: string;
 }
 }
 
 
 export const Label: SFC<Props> = props => {
 export const Label: SFC<Props> = props => {

+ 9 - 9
public/app/core/components/PermissionList/AddPermission.tsx

@@ -1,7 +1,7 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import { UserPicker } from 'app/core/components/Picker/UserPicker';
-import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
-import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
+import { UserPicker } from 'app/core/components/Select/UserPicker';
+import { TeamPicker, Team } from 'app/core/components/Select/TeamPicker';
+import { Select, SelectOptionItem } from 'app/core/components/Select/Select';
 import { User } from 'app/types';
 import { User } from 'app/types';
 import {
 import {
   dashboardPermissionLevels,
   dashboardPermissionLevels,
@@ -61,7 +61,7 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
     this.setState({ teamId: team && !Array.isArray(team) ? team.id : 0 });
     this.setState({ teamId: team && !Array.isArray(team) ? team.id : 0 });
   };
   };
 
 
-  onPermissionChanged = (permission: OptionWithDescription) => {
+  onPermissionChanged = (permission: SelectOptionItem) => {
     this.setState({ permission: permission.value });
     this.setState({ permission: permission.value });
   };
   };
 
 
@@ -121,11 +121,11 @@ class AddPermissions extends Component<Props, NewDashboardAclItem> {
             ) : null}
             ) : null}
 
 
             <div className="gf-form">
             <div className="gf-form">
-              <DescriptionPicker
-                optionsWithDesc={dashboardPermissionLevels}
-                onSelected={this.onPermissionChanged}
-                disabled={false}
-                className={'gf-form-select-box__control--menu-right'}
+              <Select
+                isSearchable={false}
+                options={dashboardPermissionLevels}
+                onChange={this.onPermissionChanged}
+                className="gf-form-select-box__control--menu-right"
               />
               />
             </div>
             </div>
 
 

+ 8 - 7
public/app/core/components/PermissionList/DisabledPermissionListItem.tsx

@@ -1,5 +1,5 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
+import Select from 'app/core/components/Select/Select';
 import { dashboardPermissionLevels } from 'app/types/acl';
 import { dashboardPermissionLevels } from 'app/types/acl';
 
 
 export interface Props {
 export interface Props {
@@ -9,6 +9,7 @@ export interface Props {
 export default class DisabledPermissionListItem extends Component<Props, any> {
 export default class DisabledPermissionListItem extends Component<Props, any> {
   render() {
   render() {
     const { item } = this.props;
     const { item } = this.props;
+    const currentPermissionLevel = dashboardPermissionLevels.find(dp => dp.value === item.permission);
 
 
     return (
     return (
       <tr className="gf-form-disabled">
       <tr className="gf-form-disabled">
@@ -23,12 +24,12 @@ export default class DisabledPermissionListItem extends Component<Props, any> {
         <td className="query-keyword">Can</td>
         <td className="query-keyword">Can</td>
         <td>
         <td>
           <div className="gf-form">
           <div className="gf-form">
-            <DescriptionPicker
-              optionsWithDesc={dashboardPermissionLevels}
-              onSelected={() => {}}
-              disabled={true}
-              className={'gf-form-select-box__control--menu-right'}
-              value={item.permission}
+            <Select
+              options={dashboardPermissionLevels}
+              onChange={() => {}}
+              isDisabled={true}
+              className="gf-form-select-box__control--menu-right"
+              value={currentPermissionLevel}
             />
             />
           </div>
           </div>
         </td>
         </td>

+ 9 - 7
public/app/core/components/PermissionList/PermissionListItem.tsx

@@ -1,5 +1,5 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
+import { Select } from 'app/core/components/Select/Select';
 import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
 import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
 import { FolderInfo } from 'app/types';
 import { FolderInfo } from 'app/types';
 
 
@@ -50,6 +50,7 @@ export default class PermissionsListItem extends PureComponent<Props> {
   render() {
   render() {
     const { item, folderInfo } = this.props;
     const { item, folderInfo } = this.props;
     const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
     const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
+    const currentPermissionLevel = dashboardPermissionLevels.find(dp => dp.value === item.permission);
 
 
     return (
     return (
       <tr className={setClassNameHelper(item.inherited)}>
       <tr className={setClassNameHelper(item.inherited)}>
@@ -74,12 +75,13 @@ export default class PermissionsListItem extends PureComponent<Props> {
         <td className="query-keyword">Can</td>
         <td className="query-keyword">Can</td>
         <td>
         <td>
           <div className="gf-form">
           <div className="gf-form">
-            <DescriptionPicker
-              optionsWithDesc={dashboardPermissionLevels}
-              onSelected={this.onPermissionChanged}
-              disabled={item.inherited}
-              className={'gf-form-select-box__control--menu-right'}
-              value={item.permission}
+            <Select
+              isSearchable={false}
+              options={dashboardPermissionLevels}
+              onChange={this.onPermissionChanged}
+              isDisabled={item.inherited}
+              className="gf-form-select-box__control--menu-right"
+              value={currentPermissionLevel}
             />
             />
           </div>
           </div>
         </td>
         </td>

+ 0 - 25
public/app/core/components/Picker/DescriptionOption.tsx

@@ -1,25 +0,0 @@
-import React from 'react';
-import { components } from 'react-select';
-import { OptionProps } from 'react-select/lib/components/Option';
-
-// https://github.com/JedWatson/react-select/issues/3038
-interface ExtendedOptionProps extends OptionProps<any> {
-  data: any;
-}
-
-export const Option = (props: ExtendedOptionProps) => {
-  const { children, isSelected, data, className } = props;
-  return (
-    <components.Option {...props}>
-      <div className={`description-picker-option__button btn btn-link ${className}`}>
-        {isSelected && <i className="fa fa-check pull-right" aria-hidden="true" />}
-        <div className="gf-form">{children}</div>
-        <div className="gf-form">
-          <div className="muted width-17">{data.description}</div>
-        </div>
-      </div>
-    </components.Option>
-  );
-};
-
-export default Option;

+ 0 - 52
public/app/core/components/Picker/DescriptionPicker.tsx

@@ -1,52 +0,0 @@
-import React, { Component } from 'react';
-import Select from 'react-select';
-import DescriptionOption from './DescriptionOption';
-import IndicatorsContainer from './IndicatorsContainer';
-import ResetStyles from './ResetStyles';
-import NoOptionsMessage from './NoOptionsMessage';
-
-export interface OptionWithDescription {
-  value: any;
-  label: string;
-  description: string;
-}
-
-export interface Props {
-  optionsWithDesc: OptionWithDescription[];
-  onSelected: (permission) => void;
-  disabled: boolean;
-  className?: string;
-  value?: any;
-}
-
-const getSelectedOption = (optionsWithDesc, value) => optionsWithDesc.find(option => option.value === value);
-
-class DescriptionPicker extends Component<Props, any> {
-  render() {
-    const { optionsWithDesc, onSelected, disabled, className, value } = this.props;
-    const selectedOption = getSelectedOption(optionsWithDesc, value);
-    return (
-      <div className="permissions-picker">
-        <Select
-          placeholder="Choose"
-          classNamePrefix={`gf-form-select-box`}
-          className={`width-7 gf-form-input gf-form-input--form-dropdown ${className || ''}`}
-          options={optionsWithDesc}
-          components={{
-            Option: DescriptionOption,
-            IndicatorsContainer,
-            NoOptionsMessage,
-          }}
-          styles={ResetStyles}
-          isDisabled={disabled}
-          onChange={onSelected}
-          getOptionValue={i => i.value}
-          getOptionLabel={i => i.label}
-          value={selectedOption}
-        />
-      </div>
-    );
-  }
-}
-
-export default DescriptionPicker;

+ 0 - 18
public/app/core/components/Picker/NoOptionsMessage.tsx

@@ -1,18 +0,0 @@
-import React from 'react';
-import { components } from 'react-select';
-import { OptionProps } from 'react-select/lib/components/Option';
-
-export interface Props {
-  children: Element;
-}
-
-export const PickerOption = (props: OptionProps<any>) => {
-  const { children, className } = props;
-  return (
-    <components.Option {...props}>
-      <div className={`description-picker-option__button btn btn-link ${className}`}>{children}</div>
-    </components.Option>
-  );
-};
-
-export default PickerOption;

+ 0 - 22
public/app/core/components/Picker/PickerOption.tsx

@@ -1,22 +0,0 @@
-import React from 'react';
-import { components } from 'react-select';
-import { OptionProps } from 'react-select/lib/components/Option';
-
-// https://github.com/JedWatson/react-select/issues/3038
-interface ExtendedOptionProps extends OptionProps<any> {
-  data: any;
-}
-
-export const PickerOption = (props: ExtendedOptionProps) => {
-  const { children, data, className } = props;
-  return (
-    <components.Option {...props}>
-      <div className={`description-picker-option__button btn btn-link ${className}`}>
-        {data.avatarUrl && <img src={data.avatarUrl} alt={data.label} className="user-picker-option__avatar" />}
-        {children}
-      </div>
-    </components.Option>
-  );
-};
-
-export default PickerOption;

+ 0 - 49
public/app/core/components/Picker/SimplePicker.tsx

@@ -1,49 +0,0 @@
-import React, { SFC } from 'react';
-import Select from 'react-select';
-import DescriptionOption from './DescriptionOption';
-import ResetStyles from './ResetStyles';
-
-interface Props {
-  className?: string;
-  defaultValue?: any;
-  getOptionLabel: (item: any) => string;
-  getOptionValue: (item: any) => string;
-  onSelected: (item: any) => {} | void;
-  options: any[];
-  placeholder?: string;
-  width: number;
-  value: any;
-}
-
-const SimplePicker: SFC<Props> = ({
-  className,
-  defaultValue,
-  getOptionLabel,
-  getOptionValue,
-  onSelected,
-  options,
-  placeholder,
-  width,
-  value,
-}) => {
-  return (
-    <Select
-      classNamePrefix={`gf-form-select-box`}
-      className={`width-${width} gf-form-input gf-form-input--form-dropdown ${className || ''}`}
-      components={{
-        Option: DescriptionOption,
-      }}
-      defaultValue={defaultValue}
-      value={value}
-      getOptionLabel={getOptionLabel}
-      getOptionValue={getOptionValue}
-      isSearchable={false}
-      onChange={onSelected}
-      options={options}
-      placeholder={placeholder || 'Choose'}
-      styles={ResetStyles}
-    />
-  );
-};
-
-export default SimplePicker;

+ 0 - 17
public/app/core/components/Picker/__snapshots__/PickerOption.test.tsx.snap

@@ -1,17 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PickerOption renders correctly 1`] = `
-<div>
-  <div
-    className="description-picker-option__button btn btn-link class-for-user-picker"
-  >
-    <img
-      alt="User picker label"
-      className="user-picker-option__avatar"
-      src="url/to/avatar"
-    />
-    Model title
-  </div>
-</div>
-`;
-  

+ 83 - 0
public/app/core/components/PluginHelp/PluginHelp.tsx

@@ -0,0 +1,83 @@
+import React, { PureComponent } from 'react';
+import Remarkable from 'remarkable';
+import { getBackendSrv } from '../../services/backend_srv';
+
+interface Props {
+  plugin: {
+    name: string;
+    id: string;
+  };
+  type: string;
+}
+
+interface State {
+  isError: boolean;
+  isLoading: boolean;
+  help: string;
+}
+
+export class PluginHelp extends PureComponent<Props, State> {
+  state = {
+    isError: false,
+    isLoading: false,
+    help: '',
+  };
+
+  componentDidMount(): void {
+    this.loadHelp();
+  }
+
+  constructPlaceholderInfo() {
+    return 'No plugin help or readme markdown file was found';
+  }
+
+  loadHelp = () => {
+    const { plugin, type } = this.props;
+    this.setState({ isLoading: true });
+
+    getBackendSrv()
+      .get(`/api/plugins/${plugin.id}/markdown/${type}`)
+      .then(response => {
+        const markdown = new Remarkable();
+        const helpHtml = markdown.render(response);
+
+        if (response === '' && type === 'help') {
+          this.setState({
+            isError: false,
+            isLoading: false,
+            help: this.constructPlaceholderInfo(),
+          });
+        } else {
+          this.setState({
+            isError: false,
+            isLoading: false,
+            help: helpHtml,
+          });
+        }
+      })
+      .catch(() => {
+        this.setState({
+          isError: true,
+          isLoading: false,
+        });
+      });
+  };
+
+  render() {
+    const { type } = this.props;
+    const { isError, isLoading, help } = this.state;
+
+    if (isLoading) {
+      return <h2>Loading help...</h2>;
+    }
+
+    if (isError) {
+      return <h3>'Error occurred when loading help'</h3>;
+    }
+
+    if (type === 'panel_help' && help === '') {
+    }
+
+    return <div className="markdown-html" dangerouslySetInnerHTML={{ __html: help }} />;
+  }
+}

+ 35 - 0
public/app/core/components/Portal/Portal.tsx

@@ -0,0 +1,35 @@
+import { PureComponent } from 'react';
+import ReactDOM from 'react-dom';
+
+interface Props {
+  className?: string;
+  root?: HTMLElement;
+}
+
+export default class BodyPortal extends PureComponent<Props> {
+  node: HTMLElement = document.createElement('div');
+  portalRoot: HTMLElement;
+
+  constructor(props) {
+    super(props);
+    const {
+      className,
+      root = document.body
+    } = this.props;
+
+    if (className) {
+      this.node.classList.add(className);
+    }
+
+    this.portalRoot = root;
+    this.portalRoot.appendChild(this.node);
+  }
+
+  componentWillUnmount() {
+    this.portalRoot.removeChild(this.node);
+  }
+
+  render() {
+    return ReactDOM.createPortal(this.props.children, this.node);
+  }
+}

+ 72 - 0
public/app/core/components/Select/DataSourcePicker.tsx

@@ -0,0 +1,72 @@
+// Libraries
+import React, { PureComponent } from 'react';
+import _ from 'lodash';
+
+// Components
+import Select from './Select';
+
+// Types
+import { DataSourceSelectItem } from 'app/types';
+
+export interface Props {
+  onChange: (ds: DataSourceSelectItem) => void;
+  datasources: DataSourceSelectItem[];
+  current: DataSourceSelectItem;
+  onBlur?: () => void;
+  autoFocus?: boolean;
+}
+
+export class DataSourcePicker extends PureComponent<Props> {
+  static defaultProps = {
+    autoFocus: false,
+  };
+
+  searchInput: HTMLElement;
+
+  constructor(props) {
+    super(props);
+  }
+
+  onChange = item => {
+    const ds = this.props.datasources.find(ds => ds.name === item.value);
+    this.props.onChange(ds);
+  };
+
+  render() {
+    const { datasources, current, autoFocus, onBlur } = this.props;
+
+    const options = datasources.map(ds => ({
+      value: ds.name,
+      label: ds.name,
+      imgUrl: ds.meta.info.logos.small,
+    }));
+
+    const value = current && {
+      label: current.name,
+      value: current.name,
+      imgUrl: current.meta.info.logos.small,
+    };
+
+    return (
+      <div className="gf-form-inline">
+        <Select
+          className="ds-picker"
+          isMulti={false}
+          isClearable={false}
+          backspaceRemovesValue={false}
+          onChange={this.onChange}
+          options={options}
+          autoFocus={autoFocus}
+          onBlur={onBlur}
+          openMenuOnFocus={true}
+          maxMenuHeight={500}
+          placeholder="Select datasource"
+          noOptionsMessage={() => 'No datasources found'}
+          value={value}
+        />
+      </div>
+    );
+  }
+}
+
+export default DataSourcePicker;

+ 2 - 2
public/app/core/components/Picker/IndicatorsContainer.tsx → public/app/core/components/Select/IndicatorsContainer.tsx

@@ -1,5 +1,5 @@
-import React from 'react';
-import { components } from 'react-select';
+import React from 'react';
+import { components } from '@torkelo/react-select';
 
 
 export const IndicatorsContainer = props => {
 export const IndicatorsContainer = props => {
   const isOpen = props.selectProps.menuIsOpen;
   const isOpen = props.selectProps.menuIsOpen;

+ 20 - 0
public/app/core/components/Select/NoOptionsMessage.tsx

@@ -0,0 +1,20 @@
+import React from 'react';
+import { components } from '@torkelo/react-select';
+import { OptionProps } from '@torkelo/react-select/lib/components/Option';
+
+export interface Props {
+  children: Element;
+}
+
+export const NoOptionsMessage = (props: OptionProps<any>) => {
+  const { children } = props;
+  return (
+    <components.Option {...props}>
+      <div className="gf-form-select-box__desc-option">
+        <div className="gf-form-select-box__desc-option__body">{children}</div>
+      </div>
+    </components.Option>
+  );
+};
+
+export default NoOptionsMessage;

+ 53 - 0
public/app/core/components/Select/OptionGroup.tsx

@@ -0,0 +1,53 @@
+import React, { PureComponent } from 'react';
+import { GroupProps } from 'react-select/lib/components/Group';
+
+interface ExtendedGroupProps extends GroupProps<any> {
+  data: any;
+}
+
+interface State {
+  expanded: boolean;
+}
+
+export default class OptionGroup extends PureComponent<ExtendedGroupProps, State> {
+  state = {
+    expanded: false,
+  };
+
+  componentDidMount() {
+    if (this.props.selectProps) {
+      const value = this.props.selectProps.value[this.props.selectProps.value.length - 1];
+
+      if (value && this.props.options.some(option => option.value === value)) {
+        this.setState({ expanded: true });
+      }
+    }
+  }
+
+  componentDidUpdate(nextProps) {
+    if (nextProps.selectProps.inputValue !== '') {
+      this.setState({ expanded: true });
+    }
+  }
+
+  onToggleChildren = () => {
+    this.setState(prevState => ({
+      expanded: !prevState.expanded,
+    }));
+  };
+
+  render() {
+    const { children, label } = this.props;
+    const { expanded } = this.state;
+
+    return (
+      <div className="gf-form-select-box__option-group">
+        <div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
+          <span className="flex-grow">{label}</span>
+          <i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
+        </div>
+        {expanded && children}
+      </div>
+    );
+  }
+}

+ 2 - 2
public/app/core/components/Picker/PickerOption.test.tsx → public/app/core/components/Select/PickerOption.test.tsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React from 'react';
 import renderer from 'react-test-renderer';
 import renderer from 'react-test-renderer';
 import PickerOption from './PickerOption';
 import PickerOption from './PickerOption';
 
 
@@ -24,7 +24,7 @@ const model = {
   children: 'Model title',
   children: 'Model title',
   data: {
   data: {
     title: 'Model title',
     title: 'Model title',
-    avatarUrl: 'url/to/avatar',
+    imgUrl: 'url/to/avatar',
     label: 'User picker label',
     label: 'User picker label',
   },
   },
   className: 'class-for-user-picker',
   className: 'class-for-user-picker',

+ 44 - 0
public/app/core/components/Select/PickerOption.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import { components } from '@torkelo/react-select';
+import { OptionProps } from 'react-select/lib/components/Option';
+
+// https://github.com/JedWatson/react-select/issues/3038
+interface ExtendedOptionProps extends OptionProps<any> {
+  data: {
+    description?: string;
+    imgUrl?: string;
+  };
+}
+
+export const Option = (props: ExtendedOptionProps) => {
+  const { children, isSelected, data } = props;
+
+  return (
+    <components.Option {...props}>
+      <div className="gf-form-select-box__desc-option">
+        {data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
+        <div className="gf-form-select-box__desc-option__body">
+          <div>{children}</div>
+          {data.description && <div className="gf-form-select-box__desc-option__desc">{data.description}</div>}
+        </div>
+        {isSelected && <i className="fa fa-check" aria-hidden="true" />}
+      </div>
+    </components.Option>
+  );
+};
+
+// was not able to type this without typescript error
+export const SingleValue = props => {
+  const { children, data } = props;
+
+  return (
+    <components.SingleValue {...props}>
+      <div className="gf-form-select-box__img-value">
+        {data.imgUrl && <img className="gf-form-select-box__desc-option__img" src={data.imgUrl} />}
+        {children}
+      </div>
+    </components.SingleValue>
+  );
+};
+
+export default Option;

+ 4 - 2
public/app/core/components/Picker/ResetStyles.tsx → public/app/core/components/Select/ResetStyles.tsx

@@ -1,4 +1,4 @@
-export default {
+export default {
   clearIndicator: () => ({}),
   clearIndicator: () => ({}),
   container: () => ({}),
   container: () => ({}),
   control: () => ({}),
   control: () => ({}),
@@ -11,7 +11,9 @@
   loadingIndicator: () => ({}),
   loadingIndicator: () => ({}),
   loadingMessage: () => ({}),
   loadingMessage: () => ({}),
   menu: () => ({}),
   menu: () => ({}),
-  menuList: () => ({}),
+  menuList: ({ maxHeight }: { maxHeight: number }) => ({
+    maxHeight,
+  }),
   multiValue: () => ({}),
   multiValue: () => ({}),
   multiValueLabel: () => ({}),
   multiValueLabel: () => ({}),
   multiValueRemove: () => ({}),
   multiValueRemove: () => ({}),

+ 232 - 0
public/app/core/components/Select/Select.tsx

@@ -0,0 +1,232 @@
+// Libraries
+import classNames from 'classnames';
+import React, { PureComponent } from 'react';
+import { default as ReactSelect } from '@torkelo/react-select';
+import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
+import { components } from '@torkelo/react-select';
+
+// Components
+import { Option, SingleValue } from './PickerOption';
+import OptionGroup from './OptionGroup';
+import IndicatorsContainer from './IndicatorsContainer';
+import NoOptionsMessage from './NoOptionsMessage';
+import ResetStyles from './ResetStyles';
+import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
+
+export interface SelectOptionItem {
+  label?: string;
+  value?: any;
+  imgUrl?: string;
+  description?: string;
+  [key: string]: any;
+}
+
+interface CommonProps {
+  defaultValue?: any;
+  getOptionLabel?: (item: SelectOptionItem) => string;
+  getOptionValue?: (item: SelectOptionItem) => string;
+  onChange: (item: SelectOptionItem) => {} | void;
+  placeholder?: string;
+  width?: number;
+  value?: SelectOptionItem;
+  className?: string;
+  isDisabled?: boolean;
+  isSearchable?: boolean;
+  isClearable?: boolean;
+  autoFocus?: boolean;
+  openMenuOnFocus?: boolean;
+  onBlur?: () => void;
+  maxMenuHeight?: number;
+  isLoading: boolean;
+  noOptionsMessage?: () => string;
+  isMulti?: boolean;
+  backspaceRemovesValue: boolean;
+}
+
+interface SelectProps {
+  options: SelectOptionItem[];
+}
+
+interface AsyncProps {
+  defaultOptions: boolean;
+  loadOptions: (query: string) => Promise<SelectOptionItem[]>;
+  loadingMessage?: () => string;
+}
+
+export const MenuList = props => {
+  return (
+    <components.MenuList {...props}>
+      <CustomScrollbar autoHide={false}>{props.children}</CustomScrollbar>
+    </components.MenuList>
+  );
+};
+
+export class Select extends PureComponent<CommonProps & SelectProps> {
+  static defaultProps = {
+    width: null,
+    className: '',
+    isDisabled: false,
+    isSearchable: true,
+    isClearable: false,
+    isMulti: false,
+    openMenuOnFocus: false,
+    autoFocus: false,
+    isLoading: false,
+    backspaceRemovesValue: true,
+    maxMenuHeight: 300,
+  };
+
+  render() {
+    const {
+      defaultValue,
+      getOptionLabel,
+      getOptionValue,
+      onChange,
+      options,
+      placeholder,
+      width,
+      value,
+      className,
+      isDisabled,
+      isLoading,
+      isSearchable,
+      isClearable,
+      backspaceRemovesValue,
+      isMulti,
+      autoFocus,
+      openMenuOnFocus,
+      onBlur,
+      maxMenuHeight,
+      noOptionsMessage,
+    } = this.props;
+
+    let widthClass = '';
+    if (width) {
+      widthClass = 'width-' + width;
+    }
+
+    const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
+
+    return (
+      <ReactSelect
+        classNamePrefix="gf-form-select-box"
+        className={selectClassNames}
+        components={{
+          Option,
+          SingleValue,
+          IndicatorsContainer,
+          MenuList,
+          Group: OptionGroup,
+        }}
+        defaultValue={defaultValue}
+        value={value}
+        getOptionLabel={getOptionLabel}
+        getOptionValue={getOptionValue}
+        menuShouldScrollIntoView={false}
+        isSearchable={isSearchable}
+        onChange={onChange}
+        options={options}
+        placeholder={placeholder || 'Choose'}
+        styles={ResetStyles}
+        isDisabled={isDisabled}
+        isLoading={isLoading}
+        isClearable={isClearable}
+        autoFocus={autoFocus}
+        onBlur={onBlur}
+        openMenuOnFocus={openMenuOnFocus}
+        maxMenuHeight={maxMenuHeight}
+        noOptionsMessage={noOptionsMessage}
+        isMulti={isMulti}
+        backspaceRemovesValue={backspaceRemovesValue}
+      />
+    );
+  }
+}
+
+export class AsyncSelect extends PureComponent<CommonProps & AsyncProps> {
+  static defaultProps = {
+    width: null,
+    className: '',
+    components: {},
+    loadingMessage: () => 'Loading...',
+    isDisabled: false,
+    isClearable: false,
+    isMulti: false,
+    isSearchable: true,
+    backspaceRemovesValue: true,
+    autoFocus: false,
+    openMenuOnFocus: false,
+    maxMenuHeight: 300,
+  };
+
+  render() {
+    const {
+      defaultValue,
+      getOptionLabel,
+      getOptionValue,
+      onChange,
+      placeholder,
+      width,
+      value,
+      className,
+      loadOptions,
+      defaultOptions,
+      isLoading,
+      loadingMessage,
+      noOptionsMessage,
+      isDisabled,
+      isSearchable,
+      isClearable,
+      backspaceRemovesValue,
+      autoFocus,
+      onBlur,
+      openMenuOnFocus,
+      maxMenuHeight,
+      isMulti,
+    } = this.props;
+
+    let widthClass = '';
+    if (width) {
+      widthClass = 'width-' + width;
+    }
+
+    const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
+
+    return (
+      <ReactAsyncSelect
+        classNamePrefix="gf-form-select-box"
+        className={selectClassNames}
+        components={{
+          Option,
+          SingleValue,
+          IndicatorsContainer,
+          NoOptionsMessage,
+        }}
+        defaultValue={defaultValue}
+        value={value}
+        getOptionLabel={getOptionLabel}
+        getOptionValue={getOptionValue}
+        menuShouldScrollIntoView={false}
+        onChange={onChange}
+        loadOptions={loadOptions}
+        isLoading={isLoading}
+        defaultOptions={defaultOptions}
+        placeholder={placeholder || 'Choose'}
+        styles={ResetStyles}
+        loadingMessage={loadingMessage}
+        noOptionsMessage={noOptionsMessage}
+        isDisabled={isDisabled}
+        isSearchable={isSearchable}
+        isClearable={isClearable}
+        autoFocus={autoFocus}
+        onBlur={onBlur}
+        openMenuOnFocus={openMenuOnFocus}
+        maxMenuHeight={maxMenuHeight}
+        isMulti={isMulti}
+        backspaceRemovesValue={backspaceRemovesValue}
+      />
+    );
+  }
+}
+
+export default Select;

+ 0 - 0
public/app/core/components/Picker/TeamPicker.test.tsx → public/app/core/components/Select/TeamPicker.test.tsx


+ 9 - 18
public/app/core/components/Picker/TeamPicker.tsx → public/app/core/components/Select/TeamPicker.tsx

@@ -1,11 +1,8 @@
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import AsyncSelect from 'react-select/lib/Async';
-import PickerOption from './PickerOption';
+import _ from 'lodash';
+import { AsyncSelect } from './Select';
 import { debounce } from 'lodash';
 import { debounce } from 'lodash';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv } from 'app/core/services/backend_srv';
-import ResetStyles from './ResetStyles';
-import IndicatorsContainer from './IndicatorsContainer';
-import NoOptionsMessage from './NoOptionsMessage';
 
 
 export interface Team {
 export interface Team {
   id: number;
   id: number;
@@ -41,13 +38,18 @@ export class TeamPicker extends Component<Props, State> {
     const backendSrv = getBackendSrv();
     const backendSrv = getBackendSrv();
     this.setState({ isLoading: true });
     this.setState({ isLoading: true });
 
 
+    if (_.isNil(query)) {
+      query = '';
+    }
+
     return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
     return backendSrv.get(`/api/teams/search?perpage=10&page=1&query=${query}`).then(result => {
       const teams = result.teams.map(team => {
       const teams = result.teams.map(team => {
         return {
         return {
           id: team.id,
           id: team.id,
+          value: team.id,
           label: team.name,
           label: team.name,
           name: team.name,
           name: team.name,
-          avatarUrl: team.avatarUrl,
+          imgUrl: team.avatarUrl,
         };
         };
       });
       });
 
 
@@ -62,24 +64,13 @@ export class TeamPicker extends Component<Props, State> {
     return (
     return (
       <div className="user-picker">
       <div className="user-picker">
         <AsyncSelect
         <AsyncSelect
-          classNamePrefix={`gf-form-select-box`}
-          isMulti={false}
           isLoading={isLoading}
           isLoading={isLoading}
           defaultOptions={true}
           defaultOptions={true}
           loadOptions={this.debouncedSearch}
           loadOptions={this.debouncedSearch}
           onChange={onSelected}
           onChange={onSelected}
-          className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
-          styles={ResetStyles}
-          components={{
-            Option: PickerOption,
-            IndicatorsContainer,
-            NoOptionsMessage,
-          }}
+          className={className}
           placeholder="Select a team"
           placeholder="Select a team"
-          loadingMessage={() => 'Loading...'}
           noOptionsMessage={() => 'No teams found'}
           noOptionsMessage={() => 'No teams found'}
-          getOptionValue={i => i.id}
-          getOptionLabel={i => i.label}
         />
         />
       </div>
       </div>
     );
     );

+ 51 - 0
public/app/core/components/Select/UnitPicker.tsx

@@ -0,0 +1,51 @@
+import React, { PureComponent } from 'react';
+import Select from './Select';
+import kbn from 'app/core/utils/kbn';
+
+interface Props {
+  onChange: (item: any) => void;
+  defaultValue?: string;
+  width?: number;
+}
+
+export default class UnitPicker extends PureComponent<Props> {
+  static defaultProps = {
+    width: 12,
+  };
+
+  render() {
+    const { defaultValue, onChange, width } = this.props;
+
+    const unitGroups = kbn.getUnitFormats();
+
+    // Need to transform the data structure to work well with Select
+    const groupOptions = unitGroups.map(group => {
+      const options = group.submenu.map(unit => {
+        return {
+          label: unit.text,
+          value: unit.value,
+        };
+      });
+
+      return {
+        label: group.text,
+        options,
+      };
+    });
+
+    const value = groupOptions.map(group => {
+      return group.options.find(option => option.value === defaultValue);
+    });
+
+    return (
+      <Select
+        width={width}
+        defaultValue={value}
+        isSearchable={true}
+        options={groupOptions}
+        placeholder="Choose"
+        onChange={onChange}
+      />
+    );
+  }
+}

+ 0 - 0
public/app/core/components/Picker/UserPicker.test.tsx → public/app/core/components/Select/UserPicker.test.tsx


+ 16 - 18
public/app/core/components/Picker/UserPicker.tsx → public/app/core/components/Select/UserPicker.tsx

@@ -1,12 +1,16 @@
+// Libraries
 import React, { Component } from 'react';
 import React, { Component } from 'react';
-import AsyncSelect from 'react-select/lib/Async';
-import PickerOption from './PickerOption';
+import _ from 'lodash';
+
+// Components
+import { AsyncSelect } from './Select';
+
+// Utils & Services
 import { debounce } from 'lodash';
 import { debounce } from 'lodash';
 import { getBackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv } from 'app/core/services/backend_srv';
+
+// Types
 import { User } from 'app/types';
 import { User } from 'app/types';
-import ResetStyles from './ResetStyles';
-import IndicatorsContainer from './IndicatorsContainer';
-import NoOptionsMessage from './NoOptionsMessage';
 
 
 export interface Props {
 export interface Props {
   onSelected: (user: User) => void;
   onSelected: (user: User) => void;
@@ -35,13 +39,18 @@ export class UserPicker extends Component<Props, State> {
     const backendSrv = getBackendSrv();
     const backendSrv = getBackendSrv();
     this.setState({ isLoading: true });
     this.setState({ isLoading: true });
 
 
+    if (_.isNil(query)) {
+      query = '';
+    }
+
     return backendSrv
     return backendSrv
       .get(`/api/org/users?query=${query}&limit=10`)
       .get(`/api/org/users?query=${query}&limit=10`)
       .then(result => {
       .then(result => {
         return result.map(user => ({
         return result.map(user => ({
           id: user.userId,
           id: user.userId,
+          value: user.userId,
           label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
           label: user.login === user.email ? user.login : `${user.login} - ${user.email}`,
-          avatarUrl: user.avatarUrl,
+          imgUrl: user.avatarUrl,
           login: user.login,
           login: user.login,
         }));
         }));
       })
       })
@@ -57,24 +66,13 @@ export class UserPicker extends Component<Props, State> {
     return (
     return (
       <div className="user-picker">
       <div className="user-picker">
         <AsyncSelect
         <AsyncSelect
-          classNamePrefix={`gf-form-select-box`}
-          isMulti={false}
+          className={className}
           isLoading={isLoading}
           isLoading={isLoading}
           defaultOptions={true}
           defaultOptions={true}
           loadOptions={this.debouncedSearch}
           loadOptions={this.debouncedSearch}
           onChange={onSelected}
           onChange={onSelected}
-          className={`gf-form-input gf-form-input--form-dropdown ${className || ''}`}
-          styles={ResetStyles}
-          components={{
-            Option: PickerOption,
-            IndicatorsContainer,
-            NoOptionsMessage,
-          }}
           placeholder="Select user"
           placeholder="Select user"
-          loadingMessage={() => 'Loading...'}
           noOptionsMessage={() => 'No users found'}
           noOptionsMessage={() => 'No users found'}
-          getOptionValue={i => i.id}
-          getOptionLabel={i => i.label}
         />
         />
       </div>
       </div>
     );
     );

+ 21 - 0
public/app/core/components/Select/__snapshots__/PickerOption.test.tsx.snap

@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PickerOption renders correctly 1`] = `
+<div>
+  <div
+    className="gf-form-select-box__desc-option"
+  >
+    <img
+      className="gf-form-select-box__desc-option__img"
+      src="url/to/avatar"
+    />
+    <div
+      className="gf-form-select-box__desc-option__body"
+    >
+      <div>
+        Model title
+      </div>
+    </div>
+  </div>
+</div>
+`;

+ 0 - 29
public/app/core/components/Picker/__snapshots__/TeamPicker.test.tsx.snap → public/app/core/components/Select/__snapshots__/TeamPicker.test.tsx.snap

@@ -57,35 +57,6 @@ exports[`TeamPicker renders correctly 1`] = `
                 }
                 }
               }
               }
               tabIndex="0"
               tabIndex="0"
-              theme={
-                Object {
-                  "borderRadius": 4,
-                  "colors": Object {
-                    "danger": "#DE350B",
-                    "dangerLight": "#FFBDAD",
-                    "neutral0": "hsl(0, 0%, 100%)",
-                    "neutral10": "hsl(0, 0%, 90%)",
-                    "neutral20": "hsl(0, 0%, 80%)",
-                    "neutral30": "hsl(0, 0%, 70%)",
-                    "neutral40": "hsl(0, 0%, 60%)",
-                    "neutral5": "hsl(0, 0%, 95%)",
-                    "neutral50": "hsl(0, 0%, 50%)",
-                    "neutral60": "hsl(0, 0%, 40%)",
-                    "neutral70": "hsl(0, 0%, 30%)",
-                    "neutral80": "hsl(0, 0%, 20%)",
-                    "neutral90": "hsl(0, 0%, 10%)",
-                    "primary": "#2684FF",
-                    "primary25": "#DEEBFF",
-                    "primary50": "#B2D4FF",
-                    "primary75": "#4C9AFF",
-                  },
-                  "spacing": Object {
-                    "baseUnit": 4,
-                    "controlHeight": 38,
-                    "menuGutter": 8,
-                  },
-                }
-              }
               type="text"
               type="text"
               value=""
               value=""
             />
             />

+ 0 - 29
public/app/core/components/Picker/__snapshots__/UserPicker.test.tsx.snap → public/app/core/components/Select/__snapshots__/UserPicker.test.tsx.snap

@@ -57,35 +57,6 @@ exports[`UserPicker renders correctly 1`] = `
                 }
                 }
               }
               }
               tabIndex="0"
               tabIndex="0"
-              theme={
-                Object {
-                  "borderRadius": 4,
-                  "colors": Object {
-                    "danger": "#DE350B",
-                    "dangerLight": "#FFBDAD",
-                    "neutral0": "hsl(0, 0%, 100%)",
-                    "neutral10": "hsl(0, 0%, 90%)",
-                    "neutral20": "hsl(0, 0%, 80%)",
-                    "neutral30": "hsl(0, 0%, 70%)",
-                    "neutral40": "hsl(0, 0%, 60%)",
-                    "neutral5": "hsl(0, 0%, 95%)",
-                    "neutral50": "hsl(0, 0%, 50%)",
-                    "neutral60": "hsl(0, 0%, 40%)",
-                    "neutral70": "hsl(0, 0%, 30%)",
-                    "neutral80": "hsl(0, 0%, 20%)",
-                    "neutral90": "hsl(0, 0%, 10%)",
-                    "primary": "#2684FF",
-                    "primary25": "#DEEBFF",
-                    "primary50": "#B2D4FF",
-                    "primary75": "#4C9AFF",
-                  },
-                  "spacing": Object {
-                    "baseUnit": 4,
-                    "controlHeight": 38,
-                    "menuGutter": 8,
-                  },
-                }
-              }
               type="text"
               type="text"
               value=""
               value=""
             />
             />

+ 13 - 15
public/app/core/components/SharedPreferences/SharedPreferences.tsx

@@ -1,7 +1,7 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
 
 
 import { Label } from 'app/core/components/Label/Label';
 import { Label } from 'app/core/components/Label/Label';
-import SimplePicker from 'app/core/components/Picker/SimplePicker';
+import Select from 'app/core/components/Select/Select';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
 
 
 import { DashboardSearchHit } from 'app/types';
 import { DashboardSearchHit } from 'app/types';
@@ -17,12 +17,12 @@ export interface State {
   dashboards: DashboardSearchHit[];
   dashboards: DashboardSearchHit[];
 }
 }
 
 
-const themes = [{ value: '', text: 'Default' }, { value: 'dark', text: 'Dark' }, { value: 'light', text: 'Light' }];
+const themes = [{ value: '', label: 'Default' }, { value: 'dark', label: 'Dark' }, { value: 'light', label: 'Light' }];
 
 
 const timezones = [
 const timezones = [
-  { value: '', text: 'Default' },
-  { value: 'browser', text: 'Local browser time' },
-  { value: 'utc', text: 'UTC' },
+  { value: '', label: 'Default' },
+  { value: 'browser', label: 'Local browser time' },
+  { value: 'utc', label: 'UTC' },
 ];
 ];
 
 
 export class SharedPreferences extends PureComponent<Props, State> {
 export class SharedPreferences extends PureComponent<Props, State> {
@@ -91,12 +91,11 @@ export class SharedPreferences extends PureComponent<Props, State> {
         <h3 className="page-heading">Preferences</h3>
         <h3 className="page-heading">Preferences</h3>
         <div className="gf-form">
         <div className="gf-form">
           <span className="gf-form-label width-11">UI Theme</span>
           <span className="gf-form-label width-11">UI Theme</span>
-          <SimplePicker
+          <Select
+            isSearchable={false}
             value={themes.find(item => item.value === theme)}
             value={themes.find(item => item.value === theme)}
             options={themes}
             options={themes}
-            getOptionValue={i => i.value}
-            getOptionLabel={i => i.text}
-            onSelected={theme => this.onThemeChanged(theme.value)}
+            onChange={theme => this.onThemeChanged(theme.value)}
             width={20}
             width={20}
           />
           />
         </div>
         </div>
@@ -107,11 +106,11 @@ export class SharedPreferences extends PureComponent<Props, State> {
           >
           >
             Home Dashboard
             Home Dashboard
           </Label>
           </Label>
-          <SimplePicker
+          <Select
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
             getOptionValue={i => i.id}
             getOptionValue={i => i.id}
             getOptionLabel={i => i.title}
             getOptionLabel={i => i.title}
-            onSelected={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
+            onChange={(dashboard: DashboardSearchHit) => this.onHomeDashboardChanged(dashboard.id)}
             options={dashboards}
             options={dashboards}
             placeholder="Chose default dashboard"
             placeholder="Chose default dashboard"
             width={20}
             width={20}
@@ -119,11 +118,10 @@ export class SharedPreferences extends PureComponent<Props, State> {
         </div>
         </div>
         <div className="gf-form">
         <div className="gf-form">
           <label className="gf-form-label width-11">Timezone</label>
           <label className="gf-form-label width-11">Timezone</label>
-          <SimplePicker
+          <Select
+            isSearchable={false}
             value={timezones.find(item => item.value === timezone)}
             value={timezones.find(item => item.value === timezone)}
-            getOptionValue={i => i.value}
-            getOptionLabel={i => i.text}
-            onSelected={timezone => this.onTimeZoneChanged(timezone.value)}
+            onChange={timezone => this.onTimeZoneChanged(timezone.value)}
             options={timezones}
             options={timezones}
             width={20}
             width={20}
           />
           />

+ 9 - 16
public/app/core/components/Switch/Switch.tsx

@@ -5,8 +5,8 @@ export interface Props {
   label: string;
   label: string;
   checked: boolean;
   checked: boolean;
   labelClass?: string;
   labelClass?: string;
-  small?: boolean;
   switchClass?: string;
   switchClass?: string;
+  transparent?: boolean;
   onChange: (event) => any;
   onChange: (event) => any;
 }
 }
 
 
@@ -25,27 +25,20 @@ export class Switch extends PureComponent<Props, State> {
   };
   };
 
 
   render() {
   render() {
-    const { labelClass = '', switchClass = '', label, checked, small } = this.props;
+    const { labelClass = '', switchClass = '', label, checked, transparent } = this.props;
+
     const labelId = `check-${this.state.id}`;
     const labelId = `check-${this.state.id}`;
-    let labelClassName = `gf-form-label ${labelClass} pointer`;
-    let switchClassName = `gf-form-switch ${switchClass}`;
-    if (small) {
-      labelClassName += ' gf-form-label--small';
-      switchClassName += ' gf-form-switch--small';
-    }
+    const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
+    const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
 
 
     return (
     return (
-      <div className="gf-form">
-        {label && (
-          <label htmlFor={labelId} className={labelClassName}>
-            {label}
-          </label>
-        )}
+      <label htmlFor={labelId} className="gf-form gf-form-switch-container">
+        {label && <div className={labelClassName}>{label}</div>}
         <div className={switchClassName}>
         <div className={switchClassName}>
           <input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
           <input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />
-          <label htmlFor={labelId} />
+          <span className="gf-form-switch__slider" />
         </div>
         </div>
-      </div>
+      </label>
     );
     );
   }
   }
 }
 }

+ 6 - 5
public/app/core/components/TagFilter/TagFilter.tsx

@@ -1,11 +1,12 @@
 import React from 'react';
 import React from 'react';
-import AsyncSelect from 'react-select/lib/Async';
+import AsyncSelect from '@torkelo/react-select/lib/Async';
+
 import { TagOption } from './TagOption';
 import { TagOption } from './TagOption';
 import { TagBadge } from './TagBadge';
 import { TagBadge } from './TagBadge';
-import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer';
-import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage';
-import { components } from 'react-select';
-import ResetStyles from 'app/core/components/Picker/ResetStyles';
+import IndicatorsContainer from 'app/core/components/Select/IndicatorsContainer';
+import NoOptionsMessage from 'app/core/components/Select/NoOptionsMessage';
+import { components } from '@torkelo/react-select';
+import ResetStyles from 'app/core/components/Select/ResetStyles';
 
 
 export interface Props {
 export interface Props {
   tags: string[];
   tags: string[];

+ 1 - 1
public/app/core/components/TagFilter/TagOption.tsx

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-import { components } from 'react-select';
+import { components } from '@torkelo/react-select';
 import { OptionProps } from 'react-select/lib/components/Option';
 import { OptionProps } from 'react-select/lib/components/Option';
 import { TagBadge } from './TagBadge';
 import { TagBadge } from './TagBadge';
 
 

+ 18 - 35
public/app/core/components/ToggleButtonGroup/ToggleButtonGroup.tsx

@@ -1,43 +1,20 @@
-import React, { SFC, ReactNode, PureComponent, ReactElement } from 'react';
+import React, { SFC, ReactNode, PureComponent } from 'react';
+import Tooltip from 'app/core/components/Tooltip/Tooltip';
 
 
 interface ToggleButtonGroupProps {
 interface ToggleButtonGroupProps {
-  onChange: (value) => void;
-  value?: any;
   label?: string;
   label?: string;
-  render: (props) => void;
+  children: JSX.Element[];
+  transparent?: boolean;
 }
 }
 
 
 export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
 export default class ToggleButtonGroup extends PureComponent<ToggleButtonGroupProps> {
-  getValues() {
-    const { children } = this.props;
-    return React.Children.toArray(children).map((c: ReactElement<any>) => c.props.value);
-  }
-
-  smallChildren() {
-    const { children } = this.props;
-    return React.Children.toArray(children).every((c: ReactElement<any>) => c.props.className.includes('small'));
-  }
-
-  handleToggle(toggleValue) {
-    const { value, onChange } = this.props;
-    if (value && value === toggleValue) {
-      return;
-    }
-    onChange(toggleValue);
-  }
-
   render() {
   render() {
-    const { value, label } = this.props;
-    const values = this.getValues();
-    const selectedValue = value || values[0];
-    const labelClassName = `gf-form-label ${this.smallChildren() ? 'small' : ''}`;
+    const { children, label, transparent } = this.props;
 
 
     return (
     return (
       <div className="gf-form">
       <div className="gf-form">
-        <div className="toggle-button-group">
-          {label && <label className={labelClassName}>{label}</label>}
-          {this.props.render({ selectedValue, onChange: this.handleToggle.bind(this) })}
-        </div>
+        {label && <label className={`gf-form-label ${transparent ? 'gf-form-label--transparent' : ''}`}>{label}</label>}
+        <div className={`toggle-button-group ${transparent ? 'toggle-button-group--transparent' : ''}`}>{children}</div>
       </div>
       </div>
     );
     );
   }
   }
@@ -49,15 +26,15 @@ interface ToggleButtonProps {
   value: any;
   value: any;
   className?: string;
   className?: string;
   children: ReactNode;
   children: ReactNode;
-  title?: string;
+  tooltip?: string;
 }
 }
 
 
 export const ToggleButton: SFC<ToggleButtonProps> = ({
 export const ToggleButton: SFC<ToggleButtonProps> = ({
   children,
   children,
   selected,
   selected,
   className = '',
   className = '',
-  title = null,
-  value,
+  value = null,
+  tooltip,
   onChange,
   onChange,
 }) => {
 }) => {
   const handleChange = event => {
   const handleChange = event => {
@@ -68,9 +45,15 @@ export const ToggleButton: SFC<ToggleButtonProps> = ({
   };
   };
 
 
   const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
   const btnClassName = `btn ${className} ${selected ? 'active' : ''}`;
-  return (
-    <button className={btnClassName} onClick={handleChange} title={title}>
+  const button = (
+    <button className={btnClassName} onClick={handleChange}>
       <span>{children}</span>
       <span>{children}</span>
     </button>
     </button>
   );
   );
+
+  if (tooltip) {
+    return <Tooltip content={tooltip}>{button}</Tooltip>;
+  } else {
+    return button;
+  }
 };
 };

+ 11 - 26
public/app/core/components/Tooltip/Popover.tsx

@@ -1,34 +1,19 @@
-import React from 'react';
-import withTooltip from './withTooltip';
-import { Target } from 'react-popper';
+import React, { PureComponent } from 'react';
+import Popper from './Popper';
+import withPopper, { UsingPopperProps } from './withPopper';
 
 
-interface PopoverProps {
-  tooltipSetState: (prevState: object) => void;
-}
-
-class Popover extends React.Component<PopoverProps, any> {
-  constructor(props) {
-    super(props);
-    this.toggleTooltip = this.toggleTooltip.bind(this);
-  }
+class Popover extends PureComponent<UsingPopperProps> {
+  render() {
+    const { children, hidePopper, showPopper, className, ...restProps } = this.props;
 
 
-  toggleTooltip() {
-    const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => {
-      return {
-        ...prevState,
-        show: !prevState.show,
-      };
-    });
-  }
+    const togglePopper = restProps.show ? hidePopper : showPopper;
 
 
-  render() {
     return (
     return (
-      <Target className="popper__target" onClick={this.toggleTooltip}>
-        {this.props.children}
-      </Target>
+      <div className={`popper__manager ${className}`} onClick={togglePopper}>
+        <Popper {...restProps}>{children}</Popper>
+      </div>
     );
     );
   }
   }
 }
 }
 
 
-export default withTooltip(Popover);
+export default withPopper(Popover);

+ 72 - 0
public/app/core/components/Tooltip/Popper.tsx

@@ -0,0 +1,72 @@
+import React, { PureComponent } from 'react';
+import Portal from 'app/core/components/Portal/Portal';
+import { Manager, Popper as ReactPopper, Reference } from 'react-popper';
+import Transition from 'react-transition-group/Transition';
+
+const defaultTransitionStyles = {
+  transition: 'opacity 200ms linear',
+  opacity: 0,
+};
+
+const transitionStyles = {
+  exited: { opacity: 0 },
+  entering: { opacity: 0 },
+  entered: { opacity: 1 },
+  exiting: { opacity: 0 },
+};
+
+interface Props {
+  renderContent: (content: any) => any;
+  show: boolean;
+  placement?: any;
+  content: string | ((props: any) => JSX.Element);
+  refClassName?: string;
+}
+
+class Popper extends PureComponent<Props> {
+  render() {
+    const { children, renderContent, show, placement, refClassName } = this.props;
+    const { content } = this.props;
+
+    return (
+      <Manager>
+        <Reference>
+          {({ ref }) => (
+            <div className={`popper_ref ${refClassName || ''}`} ref={ref}>
+              {children}
+            </div>
+          )}
+        </Reference>
+        <Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
+          {transitionState => (
+            <Portal>
+              <ReactPopper placement={placement}>
+                {({ ref, style, placement, arrowProps }) => {
+                  return (
+                    <div
+                      ref={ref}
+                      style={{
+                        ...style,
+                        ...defaultTransitionStyles,
+                        ...transitionStyles[transitionState],
+                      }}
+                      data-placement={placement}
+                      className="popper"
+                    >
+                      <div className="popper__background">
+                        {renderContent(content)}
+                        <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
+                      </div>
+                    </div>
+                  );
+                }}
+              </ReactPopper>
+            </Portal>
+          )}
+        </Transition>
+      </Manager>
+    );
+  }
+}
+
+export default Popper;

+ 9 - 28
public/app/core/components/Tooltip/Tooltip.tsx

@@ -1,36 +1,17 @@
 import React, { PureComponent } from 'react';
 import React, { PureComponent } from 'react';
-import withTooltip from './withTooltip';
-import { Target } from 'react-popper';
-
-interface Props {
-  tooltipSetState: (prevState: object) => void;
-}
-
-class Tooltip extends PureComponent<Props> {
-  showTooltip = () => {
-    const { tooltipSetState } = this.props;
-
-    tooltipSetState(prevState => ({
-      ...prevState,
-      show: true,
-    }));
-  };
-
-  hideTooltip = () => {
-    const { tooltipSetState } = this.props;
-    tooltipSetState(prevState => ({
-      ...prevState,
-      show: false,
-    }));
-  };
+import Popper from './Popper';
+import withPopper, { UsingPopperProps } from './withPopper';
 
 
+class Tooltip extends PureComponent<UsingPopperProps> {
   render() {
   render() {
+    const { children, hidePopper, showPopper, className, ...restProps } = this.props;
+
     return (
     return (
-      <Target className="popper__target" onMouseOver={this.showTooltip} onMouseOut={this.hideTooltip}>
-        {this.props.children}
-      </Target>
+      <div className={`popper__manager ${className}`} onMouseEnter={showPopper} onMouseLeave={hidePopper}>
+        <Popper {...restProps}>{children}</Popper>
+      </div>
     );
     );
   }
   }
 }
 }
 
 
-export default withTooltip(Tooltip);
+export default withPopper(Tooltip);

+ 2 - 2
public/app/core/components/Tooltip/__snapshots__/Popover.test.tsx.snap

@@ -3,10 +3,10 @@
 exports[`Popover renders correctly 1`] = `
 exports[`Popover renders correctly 1`] = `
 <div
 <div
   className="popper__manager test-class"
   className="popper__manager test-class"
+  onClick={[Function]}
 >
 >
   <div
   <div
-    className="popper__target"
-    onClick={[Function]}
+    className="popper_ref "
   >
   >
     <button>
     <button>
       Button with Popover
       Button with Popover

+ 3 - 3
public/app/core/components/Tooltip/__snapshots__/Tooltip.test.tsx.snap

@@ -3,11 +3,11 @@
 exports[`Tooltip renders correctly 1`] = `
 exports[`Tooltip renders correctly 1`] = `
 <div
 <div
   className="popper__manager test-class"
   className="popper__manager test-class"
+  onMouseEnter={[Function]}
+  onMouseLeave={[Function]}
 >
 >
   <div
   <div
-    className="popper__target"
-    onMouseOut={[Function]}
-    onMouseOver={[Function]}
+    className="popper_ref "
   >
   >
     <a
     <a
       href="http://www.grafana.com"
       href="http://www.grafana.com"

+ 88 - 0
public/app/core/components/Tooltip/withPopper.tsx

@@ -0,0 +1,88 @@
+import React from 'react';
+
+export interface UsingPopperProps {
+  showPopper: (prevState: object) => void;
+  hidePopper: (prevState: object) => void;
+  renderContent: (content: any) => any;
+  show: boolean;
+  placement?: string;
+  content: string | ((props: any) => JSX.Element);
+  className?: string;
+  refClassName?: string;
+}
+
+interface Props {
+  placement?: string;
+  className?: string;
+  refClassName?: string;
+  content: string | ((props: any) => JSX.Element);
+}
+
+interface State {
+  placement: string;
+  show: boolean;
+}
+
+export default function withPopper(WrappedComponent) {
+  return class extends React.Component<Props, State> {
+    constructor(props) {
+      super(props);
+      this.setState = this.setState.bind(this);
+      this.state = {
+        placement: this.props.placement || 'auto',
+        show: false,
+      };
+    }
+
+    componentWillReceiveProps(nextProps) {
+      if (nextProps.placement && nextProps.placement !== this.state.placement) {
+        this.setState(prevState => {
+          return {
+            ...prevState,
+            placement: nextProps.placement,
+          };
+        });
+      }
+    }
+
+    showPopper = () => {
+      this.setState(prevState => ({
+        ...prevState,
+        show: true,
+      }));
+    };
+
+    hidePopper = () => {
+      this.setState(prevState => ({
+        ...prevState,
+        show: false,
+      }));
+    };
+
+    renderContent(content) {
+      if (typeof content === 'function') {
+        // If it's a function we assume it's a React component
+        const ReactComponent = content;
+        return <ReactComponent />;
+      }
+      return content;
+    }
+
+    render() {
+      const { show, placement } = this.state;
+      const className = this.props.className || '';
+
+      return (
+        <WrappedComponent
+          {...this.props}
+          showPopper={this.showPopper}
+          hidePopper={this.hidePopper}
+          renderContent={this.renderContent}
+          show={show}
+          placement={placement}
+          className={className}
+        />
+      );
+    }
+  };
+}

+ 0 - 58
public/app/core/components/Tooltip/withTooltip.tsx

@@ -1,58 +0,0 @@
-import React from 'react';
-import { Manager, Popper, Arrow } from 'react-popper';
-
-interface IwithTooltipProps {
-  placement?: string;
-  content: string | ((props: any) => JSX.Element);
-  className?: string;
-}
-
-export default function withTooltip(WrappedComponent) {
-  return class extends React.Component<IwithTooltipProps, any> {
-    constructor(props) {
-      super(props);
-
-      this.setState = this.setState.bind(this);
-      this.state = {
-        placement: this.props.placement || 'auto',
-        show: false,
-      };
-    }
-
-    componentWillReceiveProps(nextProps) {
-      if (nextProps.placement && nextProps.placement !== this.state.placement) {
-        this.setState(prevState => {
-          return {
-            ...prevState,
-            placement: nextProps.placement,
-          };
-        });
-      }
-    }
-
-    renderContent(content) {
-      if (typeof content === 'function') {
-        // If it's a function we assume it's a React component
-        const ReactComponent = content;
-        return <ReactComponent />;
-      }
-      return content;
-    }
-
-    render() {
-      const { content, className } = this.props;
-
-      return (
-        <Manager className={`popper__manager ${className || ''}`}>
-          <WrappedComponent {...this.props} tooltipSetState={this.setState} />
-          {this.state.show ? (
-            <Popper placement={this.state.placement} className="popper">
-              {this.renderContent(content)}
-              <Arrow className="popper__arrow" />
-            </Popper>
-          ) : null}
-        </Manager>
-      );
-    }
-  };
-}

+ 3 - 3
public/app/core/components/code_editor/code_editor.ts

@@ -50,7 +50,7 @@ const DEFAULT_THEME_LIGHT = 'ace/theme/textmate';
 const DEFAULT_MODE = 'text';
 const DEFAULT_MODE = 'text';
 const DEFAULT_MAX_LINES = 10;
 const DEFAULT_MAX_LINES = 10;
 const DEFAULT_TAB_SIZE = 2;
 const DEFAULT_TAB_SIZE = 2;
-const DEFAULT_BEHAVIOURS = true;
+const DEFAULT_BEHAVIORS = true;
 const DEFAULT_SNIPPETS = true;
 const DEFAULT_SNIPPETS = true;
 
 
 const editorTemplate = `<div></div>`;
 const editorTemplate = `<div></div>`;
@@ -61,7 +61,7 @@ function link(scope, elem, attrs) {
   const maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
   const maxLines = attrs.maxLines || DEFAULT_MAX_LINES;
   const showGutter = attrs.showGutter !== undefined;
   const showGutter = attrs.showGutter !== undefined;
   const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
   const tabSize = attrs.tabSize || DEFAULT_TAB_SIZE;
-  const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIOURS;
+  const behavioursEnabled = attrs.behavioursEnabled ? attrs.behavioursEnabled === 'true' : DEFAULT_BEHAVIORS;
   const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS;
   const snippetsEnabled = attrs.snippetsEnabled ? attrs.snippetsEnabled === 'true' : DEFAULT_SNIPPETS;
 
 
   // Initialize editor
   // Initialize editor
@@ -84,7 +84,7 @@ function link(scope, elem, attrs) {
   // disable depreacation warning
   // disable depreacation warning
   codeEditor.$blockScrolling = Infinity;
   codeEditor.$blockScrolling = Infinity;
   // Padding hacks
   // Padding hacks
-  (codeEditor.renderer as any).setScrollMargin(15, 15);
+  (codeEditor.renderer as any).setScrollMargin(10, 10);
   codeEditor.renderer.setPadding(10);
   codeEditor.renderer.setPadding(10);
 
 
   setThemeMode();
   setThemeMode();

+ 9 - 22
public/app/core/components/colorpicker/ColorPicker.tsx

@@ -1,6 +1,5 @@
 import React from 'react';
 import React from 'react';
 import ReactDOM from 'react-dom';
 import ReactDOM from 'react-dom';
-import $ from 'jquery';
 import Drop from 'tether-drop';
 import Drop from 'tether-drop';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { ColorPickerPopover } from './ColorPickerPopover';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
 import { react2AngularDirective } from 'app/core/utils/react2angular';
@@ -11,29 +10,17 @@ export interface Props {
 }
 }
 
 
 export class ColorPicker extends React.Component<Props, any> {
 export class ColorPicker extends React.Component<Props, any> {
-  pickerElem: any;
+  pickerElem: HTMLElement;
   colorPickerDrop: any;
   colorPickerDrop: any;
 
 
-  constructor(props) {
-    super(props);
-    this.openColorPicker = this.openColorPicker.bind(this);
-    this.closeColorPicker = this.closeColorPicker.bind(this);
-    this.setPickerElem = this.setPickerElem.bind(this);
-    this.onColorSelect = this.onColorSelect.bind(this);
-  }
-
-  setPickerElem(elem) {
-    this.pickerElem = $(elem);
-  }
-
-  openColorPicker() {
+  openColorPicker = () => {
     const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
     const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
 
 
     const dropContentElem = document.createElement('div');
     const dropContentElem = document.createElement('div');
     ReactDOM.render(dropContent, dropContentElem);
     ReactDOM.render(dropContent, dropContentElem);
 
 
     const drop = new Drop({
     const drop = new Drop({
-      target: this.pickerElem[0],
+      target: this.pickerElem,
       content: dropContentElem,
       content: dropContentElem,
       position: 'top center',
       position: 'top center',
       classes: 'drop-popover',
       classes: 'drop-popover',
@@ -48,23 +35,23 @@ export class ColorPicker extends React.Component<Props, any> {
 
 
     this.colorPickerDrop = drop;
     this.colorPickerDrop = drop;
     this.colorPickerDrop.open();
     this.colorPickerDrop.open();
-  }
+  };
 
 
-  closeColorPicker() {
+  closeColorPicker = () => {
     setTimeout(() => {
     setTimeout(() => {
       if (this.colorPickerDrop && this.colorPickerDrop.tether) {
       if (this.colorPickerDrop && this.colorPickerDrop.tether) {
         this.colorPickerDrop.destroy();
         this.colorPickerDrop.destroy();
       }
       }
     }, 100);
     }, 100);
-  }
+  };
 
 
-  onColorSelect(color) {
+  onColorSelect = color => {
     this.props.onChange(color);
     this.props.onChange(color);
-  }
+  };
 
 
   render() {
   render() {
     return (
     return (
-      <div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={this.setPickerElem}>
+      <div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
         <div className="sp-preview">
         <div className="sp-preview">
           <div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
           <div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
         </div>
         </div>

+ 1 - 1
public/app/core/components/json_explorer/helpers.ts

@@ -1,5 +1,5 @@
 // Based on work https://github.com/mohsen1/json-formatter-js
 // Based on work https://github.com/mohsen1/json-formatter-js
-// Licence MIT, Copyright (c) 2015 Mohsen Azimi
+// License MIT, Copyright (c) 2015 Mohsen Azimi
 
 
 /*
 /*
  * Escapes `"` characters from string
  * Escapes `"` characters from string

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

@@ -1,5 +1,5 @@
 // Based on work https://github.com/mohsen1/json-formatter-js
 // Based on work https://github.com/mohsen1/json-formatter-js
-// Licence MIT, Copyright (c) 2015 Mohsen Azimi
+// License MIT, Copyright (c) 2015 Mohsen Azimi
 
 
 import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers';
 import { isObject, getObjectName, getType, getValuePreview, cssClass, createElement } from './helpers';
 
 

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