浏览代码

Merge branch 'master' into loki-query-editor

Torkel Ödegaard 7 年之前
父节点
当前提交
e4244d8bf8
共有 100 个文件被更改,包括 3044 次插入3626 次删除
  1. 10 13
      .circleci/config.yml
  2. 22 10
      CHANGELOG.md
  3. 1 1
      CODE_OF_CONDUCT.md
  4. 4 2
      Dockerfile
  5. 2 2
      PLUGIN_DEV.md
  6. 5 5
      README.md
  7. 1 1
      appveyor.yml
  8. 23 6
      conf/defaults.ini
  9. 25 0
      conf/provisioning/notifiers/sample.yaml
  10. 23 6
      conf/sample.ini
  11. 2 1
      devenv/docker/ha_test/docker-compose.yaml
  12. 69 0
      devenv/docker/loadtest/README.md
  13. 71 0
      devenv/docker/loadtest/auth_token_test.js
  14. 187 0
      devenv/docker/loadtest/modules/client.js
  15. 35 0
      devenv/docker/loadtest/modules/util.js
  16. 24 0
      devenv/docker/loadtest/run.sh
  17. 184 0
      docs/sources/administration/provisioning.md
  18. 3 0
      docs/sources/features/datasources/mssql.md
  19. 3 0
      docs/sources/features/datasources/mysql.md
  20. 3 0
      docs/sources/features/datasources/postgres.md
  21. 2 2
      docs/sources/http_api/data_source.md
  22. 26 1
      docs/sources/http_api/other.md
  23. 6 0
      docs/sources/installation/configuration.md
  24. 2 0
      packages/grafana-ui/.storybook/addons.ts
  25. 12 0
      packages/grafana-ui/.storybook/config.ts
  26. 56 0
      packages/grafana-ui/.storybook/webpack.config.js
  27. 20 3
      packages/grafana-ui/package.json
  28. 94 0
      packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx
  29. 63 0
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx
  30. 105 52
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  31. 40 0
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx
  32. 75 0
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx
  33. 102 85
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  34. 110 0
      packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx
  35. 52 0
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx
  36. 36 0
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx
  37. 39 0
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx
  38. 0 85
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx
  39. 35 14
      packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx
  40. 23 0
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx
  41. 100 0
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx
  42. 80 0
      packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx
  43. 0 72
      packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx
  44. 192 1
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  45. 29 11
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  46. 2 2
      packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap
  47. 24 0
      packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx
  48. 17 13
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  49. 1 1
      packages/grafana-ui/src/components/Select/Select.tsx
  50. 1 0
      packages/grafana-ui/src/components/Select/SelectOption.test.tsx
  51. 7 5
      packages/grafana-ui/src/components/Switch/Switch.tsx
  52. 8 5
      packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx
  53. 58 43
      packages/grafana-ui/src/components/Tooltip/Popper.tsx
  54. 27 22
      packages/grafana-ui/src/components/Tooltip/PopperController.tsx
  55. 16 1
      packages/grafana-ui/src/components/Tooltip/Tooltip.tsx
  56. 4 5
      packages/grafana-ui/src/components/Tooltip/_Tooltip.scss
  57. 2 2
      packages/grafana-ui/src/components/index.ts
  58. 0 2
      packages/grafana-ui/src/index.scss
  59. 9 0
      packages/grafana-ui/src/types/index.ts
  60. 1 8
      packages/grafana-ui/src/types/panel.ts
  61. 0 1
      packages/grafana-ui/src/utils/colors.ts
  62. 1 0
      packages/grafana-ui/src/utils/index.ts
  63. 66 0
      packages/grafana-ui/src/utils/namedColorsPalette.test.ts
  64. 182 0
      packages/grafana-ui/src/utils/namedColorsPalette.ts
  65. 6 0
      packages/grafana-ui/src/utils/propDeprecationWarning.ts
  66. 38 0
      packages/grafana-ui/src/utils/storybook/UseState.tsx
  67. 14 0
      packages/grafana-ui/src/utils/storybook/themeKnob.ts
  68. 19 0
      packages/grafana-ui/src/utils/storybook/withCenteredStory.tsx
  69. 0 509
      packages/grafana-ui/src/vendor/spectrum.css
  70. 0 2317
      packages/grafana-ui/src/vendor/spectrum.js
  71. 7 4
      packages/grafana-ui/tsconfig.json
  72. 6 6
      pkg/api/api.go
  73. 32 10
      pkg/api/common_test.go
  74. 1 1
      pkg/api/dashboard.go
  75. 2 0
      pkg/api/dtos/alerting.go
  76. 17 16
      pkg/api/http_server.go
  77. 59 67
      pkg/api/login.go
  78. 49 16
      pkg/api/login_oauth.go
  79. 2 2
      pkg/api/org_invite.go
  80. 1 1
      pkg/api/pluginproxy/ds_proxy.go
  81. 2 2
      pkg/api/signup.go
  82. 0 11
      pkg/middleware/auth.go
  83. 19 1
      pkg/middleware/auth_proxy.go
  84. 5 28
      pkg/middleware/middleware.go
  85. 48 30
      pkg/middleware/middleware_test.go
  86. 0 1
      pkg/middleware/org_redirect.go
  87. 11 13
      pkg/middleware/org_redirect_test.go
  88. 5 8
      pkg/middleware/quota_test.go
  89. 3 4
      pkg/middleware/recovery_test.go
  90. 0 21
      pkg/middleware/session.go
  91. 34 6
      pkg/models/alert_notifications.go
  92. 3 3
      pkg/models/context.go
  93. 13 3
      pkg/services/alerting/engine.go
  94. 148 0
      pkg/services/alerting/engine_integration_test.go
  95. 57 46
      pkg/services/alerting/extractor_test.go
  96. 1 1
      pkg/services/alerting/interfaces.go
  97. 6 6
      pkg/services/alerting/notifier.go
  98. 4 4
      pkg/services/alerting/notifiers/base.go
  99. 1 1
      pkg/services/alerting/notifiers/base_test.go
  100. 9 5
      pkg/services/alerting/rule.go

+ 10 - 13
.circleci/config.yml

@@ -19,7 +19,7 @@ version: 2
 jobs:
   mysql-integration-test:
     docker:
-      - image: circleci/golang:1.11.4
+      - image: circleci/golang:1.11.5
       - image: circleci/mysql:5.6-ram
         environment:
           MYSQL_ROOT_PASSWORD: rootpass
@@ -39,7 +39,7 @@ jobs:
 
   postgres-integration-test:
     docker:
-      - image: circleci/golang:1.11.4
+      - image: circleci/golang:1.11.5
       - image: circleci/postgres:9.3-ram
         environment:
           POSTGRES_USER: grafanatest
@@ -74,7 +74,7 @@ jobs:
 
   gometalinter:
     docker:
-      - image: circleci/golang:1.11.4
+      - image: circleci/golang:1.11.5
         environment:
           # we need CGO because of go-sqlite3
           CGO_ENABLED: 1
@@ -106,7 +106,7 @@ jobs:
 
   test-backend:
     docker:
-      - image: circleci/golang:1.11.4
+      - image: circleci/golang:1.11.5
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -116,7 +116,7 @@ jobs:
 
   build-all:
     docker:
-     - image: grafana/build-container:1.2.2
+     - image: grafana/build-container:1.2.3
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -147,9 +147,6 @@ jobs:
       - run:
           name: sha-sum packages
           command: 'go run build.go sha-dist'
-      - run:
-          name: Build Grafana.com master publisher
-          command: 'go build -o scripts/publish scripts/build/publish.go'
       - run:
           name: Test and build Grafana.com release publisher
           command: 'cd scripts/build/release_publisher && go test . && go build -o release_publisher .'
@@ -158,13 +155,12 @@ jobs:
           paths:
             - dist/grafana*
             - scripts/*.sh
-            - scripts/publish
             - scripts/build/release_publisher/release_publisher
             - scripts/build/publish.sh
 
   build:
     docker:
-     - image: grafana/build-container:1.2.2
+     - image: grafana/build-container:1.2.3
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -233,7 +229,7 @@ jobs:
 
   build-enterprise:
     docker:
-     - image: grafana/build-container:1.2.2
+     - image: grafana/build-container:1.2.3
     working_directory: /go/src/github.com/grafana/grafana
     steps:
       - checkout
@@ -265,7 +261,7 @@ jobs:
 
   build-all-enterprise:
     docker:
-    - image: grafana/build-container:1.2.2
+    - image: grafana/build-container:1.2.3
     working_directory: /go/src/github.com/grafana/grafana
     steps:
     - checkout
@@ -393,7 +389,8 @@ jobs:
           name: Publish to Grafana.com
           command: |
             rm dist/grafana-master-$(echo "${CIRCLE_SHA1}" | cut -b1-7).linux-x64.tar.gz
-            ./scripts/publish -apiKey ${GRAFANA_COM_API_KEY}
+            rm dist/*latest*
+            cd dist && ../scripts/build/release_publisher/release_publisher -apikey ${GRAFANA_COM_API_KEY} -from-local
 
   deploy-release:
     docker:

+ 22 - 10
CHANGELOG.md

@@ -3,34 +3,46 @@
 ### New Features
 * **Alerting**: Adds support for Google Hangouts Chat notifications [#11221](https://github.com/grafana/grafana/issues/11221), thx [@PatrickSchuster](https://github.com/PatrickSchuster)
 * **Elasticsearch**: Support bucket script pipeline aggregations [#5968](https://github.com/grafana/grafana/issues/5968)
+* **Influxdb**: Add support for time zone (`tz`) clause [#10322](https://github.com/grafana/grafana/issues/10322), thx [@cykl](https://github.com/cykl)
 * **Snapshots**: Enable deletion of public snapshot [#14109](https://github.com/grafana/grafana/issues/14109)
+* **Provisioning**: Provisioning support for alert notifiers [#10487](https://github.com/grafana/grafana/issues/10487), thx [@pbakulev](https://github.com/pbakulev)
 
 ### Minor
 
+* **Alerting**: Use seperate timeouts for alert evals and notifications [#14701](https://github.com/grafana/grafana/issues/14701), thx [@sharkpc0813](https://github.com/sharkpc0813)
 * **Elasticsearch**: Add support for offset in date histogram aggregation [#12653](https://github.com/grafana/grafana/issues/12653), thx [@mattiarossi](https://github.com/mattiarossi)
 * **Elasticsearch**: Add support for moving average and derivative using doc count (metric count) [#8843](https://github.com/grafana/grafana/issues/8843) [#11175](https://github.com/grafana/grafana/issues/11175)
+* **Elasticsearch**: Add support for template variable interpolation in alias field [#4075](https://github.com/grafana/grafana/issues/4075), thx [@SamuelToh](https://github.com/SamuelToh)
+* **Influxdb**: Fix autocomplete of measurements does not escape search string properly [#11503](https://github.com/grafana/grafana/issues/11503), thx [@SamuelToh](https://github.com/SamuelToh)
+* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
+* **Cloudwatch**: Fix Assume Role Arn [#14722](https://github.com/grafana/grafana/issues/14722), thx [@jaken551](https://github.com/jaken551)
+* **Postgres/MySQL/MSSQL**: Nanosecond timestamp support (`$__unixEpochNanoFilter`, `$__unixEpochNanoFrom`, `$__unixEpochNanoTo`) [#14711](https://github.com/grafana/grafana/pull/14711), thx [@ander26](https://github.com/ander26)
+* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
 * **Auth**: Prevent password reset when login form is disabled or either LDAP or Auth Proxy is enabled [#14246](https://github.com/grafana/grafana/issues/14246), thx [@SilverFire](https://github.com/SilverFire)
-* **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)
-* **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)
+* **Proxy whitelist**: Add CIDR capability to auth_proxy whitelist [#14546](https://github.com/grafana/grafana/issues/14546), thx [@jacobrichard](https://github.com/jacobrichard)
+* **Dashboard**: `Min width` changed to `Max per row` for repeating panels. This lets you specify the maximum number of panels to show per row and by that repeated panels will always take up full width of row [#12991](https://github.com/grafana/grafana/pull/12991), thx [@pgiraud](https://github.com/pgiraud)
+* **Dashboard**: Retain decimal precision when exporting CSV [#13929](https://github.com/grafana/grafana/issues/13929), thx [@cinaglia](https://github.com/cinaglia)
+* **Templating**: Escaping "Custom" template variables [#13754](https://github.com/grafana/grafana/issues/13754), thx [@IntegersOfK](https://github.com/IntegersOfK)
 * **Units**: Add blood glucose level units mg/dL and mmol/L [#14519](https://github.com/grafana/grafana/issues/14519), thx [@kjedamzik](https://github.com/kjedamzik)
-* **Stackdriver**: Aggregating series returns more than one series [#14581](https://github.com/grafana/grafana/issues/14581) and [#13914](https://github.com/grafana/grafana/issues/13914), thx [@kinok](https://github.com/kinok)
-* **Provisioning**: Fixes bug causing infinite growth in dashboard_version table. [#12864](https://github.com/grafana/grafana/issues/12864)
+* **Units**: Add Floating Point Operations per Second units [#14558](https://github.com/grafana/grafana/pull/14558), thx [@hahnjo](https://github.com/hahnjo)
+* **Table**: Renders epoch string as date if date column style [#14484](https://github.com/grafana/grafana/issues/14484)
+* **Piechart/Flot**: Fixes multiple piechart instances with donut bug [#15062](https://github.com/grafana/grafana/pull/15062)
+* **Dataproxy**: Override incoming Authorization header [#13815](https://github.com/grafana/grafana/issues/13815), thx [@kornholi](https://github.com/kornholi)
+* **Dataproxy**: Add global datasource proxy timeout setting [#5699](https://github.com/grafana/grafana/issues/5699), thx [@RangerRick](https://github.com/RangerRick)
+* **Database**: Support specifying database host using IPV6 for backend database and sql datasources [#13711](https://github.com/grafana/grafana/issues/13711), thx [@ellisvlad](https://github.com/ellisvlad)
 
 ### Bug fixes
 * **Search**: Fix for issue with scrolling the "tags filter" dropdown, fixes [#14486](https://github.com/grafana/grafana/issues/14486)
 * **Prometheus**: Query for annotation always uses 60s step regardless of dashboard range, fixes [#14795](https://github.com/grafana/grafana/issues/14795)
+* **Annotations**: Fix creating annotation when graph panel has no data points position the popup outside viewport [#13765](https://github.com/grafana/grafana/issues/13765), thx [@banjeremy](https://github.com/banjeremy)
 
 ### Breaking changes
-* **Text Panel**: The text panel does no longer by default allow unsantizied HTML.
-* [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags
-* they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings
-* `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable
-* `GF_PANELS_DISABLE_SANITIZE_HTML=true`.
+* **Text Panel**: The text panel does no longer by default allow unsantizied HTML. [#4117](https://github.com/grafana/grafana/issues/4117). This means that if you have text panels with scripts tags they will no longer work as before. To enable unsafe javascript execution in text panels enable the settings `disable_sanitize_html` under the section `[panels]` in your Grafana ini file, or set env variable  `GF_PANELS_DISABLE_SANITIZE_HTML=true`.
+* **Dashboard**: Panel property `minSpan` replaced by `maxPerRow`. Dashboard migration will automatically migrate all dashboard panels using the `minSpan` property to the new `maxPerRow` property [#12991](https://github.com/grafana/grafana/pull/12991)
 
 # 5.4.3 (2019-01-14)
 

+ 1 - 1
CODE_OF_CONDUCT.md

@@ -2,7 +2,7 @@
 
 ## Our Pledge
 
-In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
 
 ## Our Standards
 

+ 4 - 2
Dockerfile

@@ -1,5 +1,5 @@
 # Golang build container
-FROM golang:1.11.4
+FROM golang:1.11.5
 
 WORKDIR $GOPATH/src/github.com/grafana/grafana
 
@@ -19,11 +19,13 @@ COPY package.json package.json
 RUN go run build.go build
 
 # Node build container
-FROM node:8
+FROM node:10.14.2
 
 WORKDIR /usr/src/app/
 
 COPY package.json yarn.lock ./
+COPY packages packages
+
 RUN yarn install --pure-lockfile --no-progress
 
 COPY Gruntfile.js tsconfig.json tslint.json ./

+ 2 - 2
PLUGIN_DEV.md

@@ -1,7 +1,7 @@
 # Plugin Development 
 
-This document is not meant as complete guide for developing plugins but more as a changelog for changes in
-Grafana that can impact plugin development. When ever you as plugin author encounter an issue with your plugin after
+This document is not meant as a complete guide for developing plugins but more as a changelog for changes in
+Grafana that can impact plugin development. Whenever you as a plugin author encounter an issue with your plugin after
 upgrading Grafana please check here before creating an issue. 
 
 ## Links

+ 5 - 5
README.md

@@ -19,7 +19,7 @@ If you have any problems please read the [troubleshooting guide](http://docs.gra
 Be sure to read the [getting started guide](http://docs.grafana.org/guides/gettingstarted/) and the other feature guides.
 
 ## Run from master
-If you want to build a package yourself, or contribute - Here is a guide for how to do that. You can always find
+If you want to build a package yourself, or contribute - here is a guide for how to do that. You can always find
 the latest master builds [here](https://grafana.com/grafana/download)
 
 ### Dependencies
@@ -71,7 +71,7 @@ Open grafana in your browser (default: `http://localhost:3000`) and login with a
 
 ### Building a Docker image
 
-There are two different ways to build a Grafana docker image. If you're machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
+There are two different ways to build a Grafana docker image. If your machine is setup for Grafana development and you run linux/amd64 you can build just the image. Otherwise, there is the option to build Grafana completely within Docker.
 
 Run the image you have built using: `docker run --rm -p 3000:3000 grafana/grafana:dev`
 
@@ -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`
 
-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`
+Notice: If you are using Docker for MacOS, be sure to set the memory limit to be larger than 2 GiB (at docker -> Preferences -> Advanced), otherwise `grunt build` may fail.
 
 ### Dev config
 
@@ -129,8 +129,8 @@ GRAFANA_TEST_DB=postgres go test ./pkg/...
 
 ## Contribute
 
-If you have any idea for an improvement or found a bug, do not hesitate to open an issue.
-And if you have time clone this repo and submit a pull request and help me make Grafana
+If you have any ideas for improvement or have found a bug, do not hesitate to open an issue.
+And if you have time, clone this repo and submit a pull request to help me make Grafana
 the kickass metrics & devops dashboard we all dream about! 
 
 Read the [contributing](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide then check the [`beginner friendly`](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+label%3A%22beginner+friendly%22) label to find issues that are easy and that we would like help with.

+ 1 - 1
appveyor.yml

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

+ 23 - 6
conf/defaults.ini

@@ -106,6 +106,22 @@ path = grafana.db
 # For "sqlite3" only. cache mode setting used for connecting to the database
 cache_mode = private
 
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+login_remember_days = 7
+
+# How often should the login token be rotated. default to '10m'
+rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+delete_expired_token_after_days = 30
+
 #################################### Session #############################
 [session]
 # Either "memory", "file", "redis", "mysql", "postgres", "memcache", default is "file"
@@ -143,6 +159,9 @@ conn_max_lifetime = 14400
 # This enables data proxy logging, default is false
 logging = false
 
+# How long the data proxy should wait before timing out default is 30 (seconds)
+timeout = 30
+
 #################################### Analytics ###########################
 [analytics]
 # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -175,11 +194,6 @@ admin_password = admin
 # used for signing
 secret_key = SW2YcwTIb9zpOOhoPsMm
 
-# Auto-login remember days
-login_remember_days = 7
-cookie_username = grafana_user
-cookie_remember_name = grafana_remember
-
 # disable gravatar profile images
 disable_gravatar = false
 
@@ -189,6 +203,9 @@ data_source_proxy_whitelist =
 # disable protection against brute force login attempts
 disable_brute_force_login_protection = false
 
+# set cookies as https only. default is false
+https_flag_cookies = false
+
 #################################### Snapshots ###########################
 [snapshots]
 # snapshot sharing options
@@ -490,7 +507,7 @@ concurrent_render_limit = 5
 #################################### Explore #############################
 [explore]
 # Enable the Explore section
-enabled = false
+enabled = true
 
 #################################### Internal Grafana Metrics ############
 # Metrics available at HTTP API Url /metrics

+ 25 - 0
conf/provisioning/notifiers/sample.yaml

@@ -0,0 +1,25 @@
+# # config file version
+apiVersion: 1
+
+# notifiers:
+#   - name: default-slack-temp
+#     type: slack
+#     org_name: Main Org.
+#     is_default: true
+#     uid: notifier1
+#     settings:
+#       recipient: "XXX"
+#       token: "xoxb"
+#       uploadImage: true
+#       url: https://slack.com
+#   - name: default-email
+#     type: email
+#     org_id: 1
+#     uid: notifier2
+#     is_default: false  
+#     settings:
+#       addresses: example11111@example.com
+# delete_notifiers:
+#   - name: default-slack-temp
+#     org_name: Main Org.
+#     uid: notifier1

+ 23 - 6
conf/sample.ini

@@ -102,6 +102,22 @@ log_queries =
 # For "sqlite3" only. cache mode setting used for connecting to the database. (private, shared)
 ;cache_mode = private
 
+#################################### Login ###############################
+
+[login]
+
+# Login cookie name
+;cookie_name = grafana_session
+
+# How many days an session can be unused before we inactivate it
+;login_remember_days = 7
+
+# How often should the login token be rotated. default to '10'
+;rotate_token_minutes = 10
+
+# How long should Grafana keep expired tokens before deleting them
+;delete_expired_token_after_days = 30
+
 #################################### Session ####################################
 [session]
 # Either "memory", "file", "redis", "mysql", "postgres", default is "file"
@@ -130,6 +146,9 @@ log_queries =
 # This enables data proxy logging, default is false
 ;logging = false
 
+# How long the data proxy should wait before timing out default is 30 (seconds)
+;timeout = 30
+
 #################################### Analytics ####################################
 [analytics]
 # Server reporting, sends usage counters to stats.grafana.org every 24 hours.
@@ -162,11 +181,6 @@ log_queries =
 # used for signing
 ;secret_key = SW2YcwTIb9zpOOhoPsMm
 
-# Auto-login remember days
-;login_remember_days = 7
-;cookie_username = grafana_user
-;cookie_remember_name = grafana_remember
-
 # disable gravatar profile images
 ;disable_gravatar = false
 
@@ -176,6 +190,9 @@ log_queries =
 # disable protection against brute force login attempts
 ;disable_brute_force_login_protection = false
 
+# set cookies as https only. default is false
+;https_flag_cookies = false
+
 #################################### Snapshots ###########################
 [snapshots]
 # snapshot sharing options
@@ -415,7 +432,7 @@ log_queries =
 #################################### Explore #############################
 [explore]
 # Enable the Explore section
-;enabled = false
+;enabled = true
 
 #################################### Internal Grafana Metrics ##########################
 # Metrics available at HTTP API Url /metrics

+ 2 - 1
devenv/docker/ha_test/docker-compose.yaml

@@ -54,7 +54,8 @@ services:
       # - GF_DATABASE_SSL_MODE=disable
       # - GF_SESSION_PROVIDER=postgres
       # - GF_SESSION_PROVIDER_CONFIG=user=grafana password=password host=db port=5432 dbname=grafana sslmode=disable
-      - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug
+      - GF_LOG_FILTERS=alerting.notifier:debug,alerting.notifier.slack:debug,auth:debug
+      - GF_LOGIN_ROTATE_TOKEN_MINUTES=2
     ports:
       - 3000
     depends_on:

+ 69 - 0
devenv/docker/loadtest/README.md

@@ -0,0 +1,69 @@
+# Grafana load test
+
+Runs load tests and checks using [k6](https://k6.io/).
+
+## Prerequisites
+
+Docker
+
+## Run
+
+Run load test for 15 minutes:
+
+```bash
+$ ./run.sh
+```
+
+Run load test for custom duration:
+
+```bash
+$ ./run.sh -d 10s
+```
+
+Example output:
+
+```bash
+
+          /\      |‾‾|  /‾‾/  /‾/
+     /\  /  \     |  |_/  /  / /
+    /  \/    \    |      |  /  ‾‾\
+   /          \   |  |‾\  \ | (_) |
+  / __________ \  |__|  \__\ \___/ .io
+
+  execution: local
+     output: -
+     script: src/auth_token_test.js
+
+    duration: 15m0s, iterations: -
+         vus: 2,     max: 2
+
+    done [==========================================================] 15m0s / 15m0s
+
+    █ user auth token test
+
+      █ user authenticates thru ui with username and password
+
+        ✓ response status is 200
+        ✓ response has cookie 'grafana_session' with 32 characters
+
+      █ batch tsdb requests
+
+        ✓ response status is 200
+
+    checks.....................: 100.00% ✓ 32844 ✗ 0
+    data_received..............: 411 MB  457 kB/s
+    data_sent..................: 12 MB   14 kB/s
+    group_duration.............: avg=95.64ms  min=16.42ms  med=94.35ms  max=307.52ms p(90)=137.78ms p(95)=146.75ms
+    http_req_blocked...........: avg=1.27ms   min=942ns    med=610.08µs max=48.32ms  p(90)=2.92ms   p(95)=4.25ms
+    http_req_connecting........: avg=1.06ms   min=0s       med=456.79µs max=47.19ms  p(90)=2.55ms   p(95)=3.78ms
+    http_req_duration..........: avg=58.16ms  min=1ms      med=52.59ms  max=293.35ms p(90)=109.53ms p(95)=120.19ms
+    http_req_receiving.........: avg=38.98µs  min=6.43µs   med=32.55µs  max=16.2ms   p(90)=64.63µs  p(95)=78.8µs
+    http_req_sending...........: avg=328.66µs min=8.09µs   med=110.77µs max=44.13ms  p(90)=552.65µs p(95)=1.09ms
+    http_req_tls_handshaking...: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s
+    http_req_waiting...........: avg=57.79ms  min=935.02µs med=52.15ms  max=293.06ms p(90)=109.04ms p(95)=119.71ms
+    http_reqs..................: 34486   38.317775/s
+    iteration_duration.........: avg=1.09s    min=1.81µs   med=1.09s    max=1.3s     p(90)=1.13s    p(95)=1.14s
+    iterations.................: 1642    1.824444/s
+    vus........................: 2       min=2   max=2
+    vus_max....................: 2       min=2   max=2
+```

+ 71 - 0
devenv/docker/loadtest/auth_token_test.js

@@ -0,0 +1,71 @@
+import { sleep, check, group } from 'k6';
+import { createClient, createBasicAuthClient } from './modules/client.js';
+import { createTestOrgIfNotExists, createTestdataDatasourceIfNotExists } from './modules/util.js';
+
+export let options = {
+  noCookiesReset: true
+};
+
+let endpoint = __ENV.URL || 'http://localhost:3000';
+const client = createClient(endpoint);
+
+export const setup = () => {
+  const basicAuthClient = createBasicAuthClient(endpoint, 'admin', 'admin');
+  const orgId = createTestOrgIfNotExists(basicAuthClient);
+  const datasourceId = createTestdataDatasourceIfNotExists(basicAuthClient);
+  client.withOrgId(orgId);
+  return {
+    orgId: orgId,
+    datasourceId: datasourceId,
+  };
+}
+
+export default (data) => {
+  group("user auth token test", () => {
+    if (__ITER === 0) {
+      group("user authenticates thru ui with username and password", () => {
+        let res = client.ui.login('admin', 'admin');
+
+        check(res, {
+          'response status is 200': (r) => r.status === 200,
+          'response has cookie \'grafana_session\' with 32 characters': (r) => r.cookies.grafana_session[0].value.length === 32,
+        });
+      });
+    }
+
+    if (__ITER !== 0) {
+      group("batch tsdb requests", () => {
+        const batchCount = 20;
+        const requests = [];
+        const payload = {
+          from: '1547765247624',
+          to: '1547768847624',
+          queries: [{
+            refId: 'A',
+            scenarioId: 'random_walk',
+            intervalMs: 10000,
+            maxDataPoints: 433,
+            datasourceId: data.datasourceId,
+          }]
+        };
+
+        requests.push({ method: 'GET', url: '/api/annotations?dashboardId=2074&from=1548078832772&to=1548082432772' });
+
+        for (let n = 0; n < batchCount; n++) {
+          requests.push({ method: 'POST', url: '/api/tsdb/query', body: payload });
+        }
+
+        let responses = client.batch(requests);
+        for (let n = 0; n < batchCount; n++) {
+          check(responses[n], {
+            'response status is 200': (r) => r.status === 200,
+          });
+        }
+      });
+    }
+  });
+
+  sleep(1)
+}
+
+export const teardown = (data) => {}

+ 187 - 0
devenv/docker/loadtest/modules/client.js

@@ -0,0 +1,187 @@
+import http from "k6/http";
+import encoding from 'k6/encoding';
+
+export const UIEndpoint = class UIEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  login(username, pwd) {
+    const payload = { user: username, password: pwd };
+    return this.httpClient.formPost('/login', payload);
+  }
+}
+
+export const DatasourcesEndpoint = class DatasourcesEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  getById(id) {
+    return this.httpClient.get(`/datasources/${id}`);
+  }
+
+  getByName(name) {
+    return this.httpClient.get(`/datasources/name/${name}`);
+  }
+
+  create(payload) {
+    return this.httpClient.post(`/datasources`, JSON.stringify(payload));
+  }
+
+  delete(id) {
+    return this.httpClient.delete(`/datasources/${id}`);
+  }
+}
+
+export const OrganizationsEndpoint = class OrganizationsEndpoint {
+  constructor(httpClient) {
+    this.httpClient = httpClient;
+  }
+
+  getById(id) {
+    return this.httpClient.get(`/orgs/${id}`);
+  }
+
+  getByName(name) {
+    return this.httpClient.get(`/orgs/name/${name}`);
+  }
+
+  create(name) {
+    let payload = {
+      name: name,
+    };
+    return this.httpClient.post(`/orgs`, JSON.stringify(payload));
+  }
+
+  delete(id) {
+    return this.httpClient.delete(`/orgs/${id}`);
+  }
+}
+
+export const GrafanaClient = class GrafanaClient {
+  constructor(httpClient) {
+    httpClient.onBeforeRequest = this.onBeforeRequest;
+    this.raw = httpClient;
+    this.ui = new UIEndpoint(httpClient);
+    this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
+    this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
+  }
+
+  batch(requests) {
+    return this.raw.batch(requests);
+  }
+
+  withOrgId(orgId) {
+    this.orgId = orgId;
+  }
+
+  onBeforeRequest(params) {
+    if (this.orgId && this.orgId > 0) {
+      params = params.headers || {};
+      params.headers["X-Grafana-Org-Id"] = this.orgId;
+    }
+  }
+}
+
+export const BaseClient = class BaseClient {
+  constructor(url, subUrl) {
+    if (url.endsWith('/')) {
+      url = url.substring(0, url.length - 1);
+    }
+
+    if (subUrl.endsWith('/')) {
+      subUrl = subUrl.substring(0, subUrl.length - 1);
+    }
+
+    this.url = url + subUrl;
+    this.onBeforeRequest = () => {};
+  }
+
+  withUrl(subUrl) {
+    let c = new BaseClient(this.url,  subUrl);
+    c.onBeforeRequest = this.onBeforeRequest;
+    return c;
+  }
+
+  beforeRequest(params) {
+
+  }
+
+  get(url, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.get(this.url + url, params);
+  }
+
+  formPost(url, body, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.post(this.url + url, body, params);
+  }
+
+  post(url, body, params) {
+    params = params || {};
+    params.headers = params.headers || {};
+    params.headers['Content-Type'] = 'application/json';
+
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.post(this.url + url, body, params);
+  }
+
+  delete(url, params) {
+    params = params || {};
+    this.beforeRequest(params);
+    this.onBeforeRequest(params);
+    return http.del(this.url + url, null, params);
+  }
+
+  batch(requests) {
+    for (let n = 0; n < requests.length; n++) {
+      let params = requests[n].params || {};
+      params.headers = params.headers || {};
+      params.headers['Content-Type'] = 'application/json';
+      this.beforeRequest(params);
+      this.onBeforeRequest(params);
+      requests[n].params = params;
+      requests[n].url = this.url + requests[n].url;
+      if (requests[n].body) {
+        requests[n].body = JSON.stringify(requests[n].body);
+      }
+    }
+
+    return http.batch(requests);
+  }
+}
+
+export class BasicAuthClient extends BaseClient {
+  constructor(url, subUrl, username, password) {
+    super(url, subUrl);
+    this.username = username;
+    this.password = password;
+  }
+
+  withUrl(subUrl) {
+    let c = new BasicAuthClient(this.url,  subUrl, this.username, this.password);
+    c.onBeforeRequest = this.onBeforeRequest;
+    return c;
+  }
+
+  beforeRequest(params) {
+    params = params || {};
+    params.headers = params.headers || {};
+    let token = `${this.username}:${this.password}`;
+    params.headers['Authorization'] = `Basic ${encoding.b64encode(token)}`;
+  }
+}
+
+export const createClient = (url) => {
+  return new GrafanaClient(new BaseClient(url, ''));
+}
+
+export const createBasicAuthClient = (url, username, password) => {
+  return new GrafanaClient(new BasicAuthClient(url, '', username, password));
+}

+ 35 - 0
devenv/docker/loadtest/modules/util.js

@@ -0,0 +1,35 @@
+export const createTestOrgIfNotExists = (client) => {
+  let orgId = 0;
+  let res = client.orgs.getByName('k6');
+  if (res.status === 404) {
+    res = client.orgs.create('k6');
+    if (res.status !== 200) {
+      throw new Error('Expected 200 response status when creating org');
+    }
+    orgId = res.json().orgId;
+  } else {
+    orgId = res.json().id;
+  }
+
+  client.withOrgId(orgId);
+  return orgId;
+}
+
+export const createTestdataDatasourceIfNotExists = (client) => {
+  const payload = {
+    access: 'proxy',
+    isDefault: false,
+    name: 'k6-testdata',
+    type: 'testdata',
+  };
+
+  let res = client.datasources.getByName(payload.name);
+  if (res.status === 404) {
+    res = client.datasources.create(payload);
+    if (res.status !== 200) {
+      throw new Error('Expected 200 response status when creating datasource');
+    }
+  }
+
+  return res.json().id;
+}

+ 24 - 0
devenv/docker/loadtest/run.sh

@@ -0,0 +1,24 @@
+#/bin/bash
+
+PWD=$(pwd)
+
+run() {
+  duration='15m'
+  url='http://localhost:3000'
+
+  while getopts ":d:u:" o; do
+    case "${o}" in
+				d)
+            duration=${OPTARG}
+            ;;
+        u)
+            url=${OPTARG}
+            ;;
+    esac
+	done
+	shift $((OPTIND-1))
+
+  docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus 2 --duration $duration src/auth_token_test.js
+}
+
+run "$@"

+ 184 - 0
docs/sources/administration/provisioning.md

@@ -231,3 +231,187 @@ By default Grafana will delete dashboards in the database if the file is removed
 > which leads to problems if you re-use settings that are supposed to be unique.
 > Be careful not to re-use the same `title` multiple times within a folder
 > or `uid` within the same installation as this will cause weird behaviors.
+
+## Alert Notification Channels
+
+Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/installation/configuration/#provisioning) directory.
+
+Each config file can contain the following top-level fields:
+- `notifiers`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file.
+- `delete_notifiers`, a list of alert notifications to be deleted before before inserting/updating those in the `notifiers` list.
+
+Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid.
+
+By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name.
+
+```json
+{
+  ...
+      "alert": {
+        ...,
+        "conditions": [...],
+        "frequency": "24h",
+        "noDataState": "ok",
+        "notifications": [
+           {"uid": "notifier1"},
+           {"uid": "notifier2"},
+        ]
+      }
+  ...
+}
+```
+
+### Example Alert Notification Channels Config File
+
+```yaml
+notifiers:
+  - name: notification-channel-1
+    type: slack
+    uid: notifier1
+    # either
+    org_id: 2
+    # or
+    org_name: Main Org.
+    is_default: true
+    # See `Supported Settings` section for settings supporter for each
+    # alert notification type.
+    settings:
+      recipient: "XXX"
+      token: "xoxb"
+      uploadImage: true
+      url: https://slack.com
+
+delete_notifiers:
+  - name: notification-channel-1
+    uid: notifier1
+    # either
+    org_id: 2
+    # or 
+    org_name: Main Org.
+  - name: notification-channel-2
+    # default org_id: 1
+```
+
+### Supported Settings
+
+The following sections detail the supported settings for each alert notification type.
+
+#### Alert notification `pushover`
+
+| Name |
+| ---- |
+| apiToken |
+| userKey |
+| device |
+| retry |
+| expire |
+
+#### Alert notification `slack`
+
+| Name |
+| ---- |
+| url |
+| recipient |
+| username |
+| iconEmoji |
+| iconUrl |
+| uploadImage |
+| mention |
+| token |
+
+#### Alert notification `victorops`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `kafka`
+
+| Name |
+| ---- |
+| kafkaRestProxy |
+| kafkaTopic |
+
+#### Alert notification `LINE`
+
+| Name |
+| ---- |
+| token |
+
+#### Alert notification `pagerduty`
+
+| Name |
+| ---- |
+| integrationKey |
+
+#### Alert notification `sensu`
+
+| Name |
+| ---- |
+| url |
+| source |
+| handler |
+| username |
+| password |
+
+#### Alert notification `prometheus-alertmanager`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `teams`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `dingding`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `email`
+
+| Name |
+| ---- |
+| addresses |
+
+#### Alert notification `hipchat`
+
+| Name |
+| ---- |
+| url |
+| apikey |
+| roomid |
+
+#### Alert notification `opsgenie`
+
+| Name |
+| ---- |
+| apiKey |
+| apiUrl |
+
+#### Alert notification `telegram`
+
+| Name |
+| ---- |
+| bottoken |
+| chatid |
+
+#### Alert notification `threema`
+
+| Name |
+| ---- |
+| gateway_id |
+| recipient_id |
+| api_secret |
+
+#### Alert notification `webhook`
+
+| Name |
+| ---- |
+| url |
+| username |
+| password |

+ 3 - 0
docs/sources/features/datasources/mssql.md

@@ -110,6 +110,9 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamp. For example, *dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872*
+*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
+*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as nanosecond timestamp. For example, *1494497183142514872*
 *$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
 *$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 

+ 3 - 0
docs/sources/features/datasources/mysql.md

@@ -144,6 +144,9 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamp. For example, *dateColumn > 1494410783152415214 AND dateColumn < 1494497183142514872*
+*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
+*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as nanosecond timestamp. For example, *1494497183142514872*
 *$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup but for times stored as unix timestamp (only available in Grafana 5.3+).
 *$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above but also adds a column alias (only available in Grafana 5.3+).
 

+ 3 - 0
docs/sources/features/datasources/postgres.md

@@ -154,6 +154,9 @@ Macro example | Description
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamps. For example, *dateColumn >= 1494410783 AND dateColumn <= 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
+*$__unixEpochNanoFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as nanosecond timestamps. For example, *dateColumn >= 1494410783152415214 AND dateColumn <= 1494497183142514872*
+*$__unixEpochNanoFrom()* | Will be replaced by the start of the currently active time selection as nanosecond timestamp. For example, *1494410783152415214*
+*$__unixEpochNanoTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183142514872*
 *$__unixEpochGroup(dateColumn,'5m', [fillmode])* | Same as $__timeGroup, but for times stored as unix timestamp (only available in Grafana 5.3+).
 *$__unixEpochGroupAlias(dateColumn,'5m', [fillmode])* | Same as above, but also adds a column alias (only available in Grafana 5.3+).
 

+ 2 - 2
docs/sources/http_api/data_source.md

@@ -188,8 +188,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
     "defaultRegion": "us-west-1"
   },
   "secureJsonData": {
-    "accessKey": "Ol4pIDpeKSA6XikgOl4p", //should not be encoded
-    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs" //should be Base-64 encoded
+    "accessKey": "Ol4pIDpeKSA6XikgOl4p",
+    "secretKey": "dGVzdCBrZXkgYmxlYXNlIGRvbid0IHN0ZWFs"
   }
 }
 ```

+ 26 - 1
docs/sources/http_api/other.md

@@ -82,4 +82,29 @@ HTTP/1.1 200
 Content-Type: application/json
 
 {"message": "Logged in"}
-```
+```
+
+# Health API
+
+## Returns health information about Grafana
+
+`GET /api/health`
+
+**Example Request**
+
+```http
+GET /api/health
+Accept: application/json
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200 OK
+
+{
+  "commit": "087143285",
+  "database": "ok",
+  "version": "5.1.3"
+}
+```

+ 6 - 0
docs/sources/installation/configuration.md

@@ -391,6 +391,12 @@ value is `true`.
 If you want to track Grafana usage via Google analytics specify *your* Universal
 Analytics ID here. By default this feature is disabled.
 
+### check_for_updates
+
+Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used
+in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor
+send any sensitive information.
+
 <hr />
 
 ## [dashboards]

+ 2 - 0
packages/grafana-ui/.storybook/addons.ts

@@ -0,0 +1,2 @@
+import '@storybook/addon-knobs/register';
+import '@storybook/addon-actions/register';

+ 12 - 0
packages/grafana-ui/.storybook/config.ts

@@ -0,0 +1,12 @@
+import { configure } from '@storybook/react';
+
+import '../../../public/sass/grafana.light.scss';
+
+// automatically import all files ending in *.stories.tsx
+const req = require.context('../src/components', true, /.story.tsx$/);
+
+function loadStories() {
+  req.keys().forEach(req);
+}
+
+configure(loadStories, module);

+ 56 - 0
packages/grafana-ui/.storybook/webpack.config.js

@@ -0,0 +1,56 @@
+const path = require('path');
+
+module.exports = (baseConfig, env, config) => {
+
+  config.module.rules.push({
+    test: /\.(ts|tsx)$/,
+    use: [
+      {
+        loader: require.resolve('awesome-typescript-loader'),
+      },
+    ],
+  });
+
+  config.module.rules.push({
+    test: /\.scss$/,
+    use: [
+      {
+        loader: 'style-loader',
+      },
+      {
+        loader: 'css-loader',
+        options: {
+          importLoaders: 2,
+          url: false,
+          sourceMap: false,
+          minimize: false,
+        },
+      },
+      {
+        loader: 'postcss-loader',
+        options: {
+          sourceMap: false,
+          config: { path: __dirname + '../../../../scripts/webpack/postcss.config.js' },
+        },
+      },
+      { loader: 'sass-loader', options: { sourceMap: false } },
+    ],
+  });
+
+  config.module.rules.push({
+    test: require.resolve('jquery'),
+    use: [
+      {
+        loader: 'expose-loader',
+        query: 'jQuery',
+      },
+      {
+        loader: 'expose-loader',
+        query: '$',
+      },
+    ],
+  });
+
+  config.resolve.extensions.push('.ts', '.tsx');
+  return config;
+};

+ 20 - 3
packages/grafana-ui/package.json

@@ -5,19 +5,20 @@
   "main": "src/index.ts",
   "scripts": {
     "tslint": "tslint -c tslint.json --project tsconfig.json",
-    "typecheck": "tsc --noEmit"
+    "typecheck": "tsc --noEmit",
+    "storybook": "start-storybook -p 9001 -c .storybook -s ../../public"
   },
   "author": "",
   "license": "ISC",
   "dependencies": {
     "@torkelo/react-select": "2.1.1",
-    "@types/react-test-renderer": "^16.0.3",
-    "@types/react-transition-group": "^2.0.15",
+    "@types/react-color": "^2.14.0",
     "classnames": "^2.2.5",
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
     "moment": "^2.22.2",
     "react": "^16.6.3",
+    "react-color": "^2.17.0",
     "react-custom-scrollbars": "^4.2.1",
     "react-dom": "^16.6.3",
     "react-highlight-words": "0.11.0",
@@ -29,16 +30,32 @@
     "tinycolor2": "^1.4.1"
   },
   "devDependencies": {
+    "@storybook/addon-actions": "^4.1.7",
+    "@storybook/addon-info": "^4.1.6",
+    "@storybook/addon-knobs": "^4.1.7",
+    "@storybook/react": "^4.1.4",
     "@types/classnames": "^2.2.6",
     "@types/jest": "^23.3.2",
     "@types/jquery": "^1.10.35",
     "@types/lodash": "^4.14.119",
+    "@types/node": "^10.12.18",
     "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-test-renderer": "^16.0.3",
+    "@types/react-transition-group": "^2.0.15",
+    "@types/storybook__addon-actions": "^3.4.1",
+    "@types/storybook__addon-info": "^3.4.2",
+    "@types/storybook__addon-knobs": "^4.0.0",
+    "@types/storybook__react": "^4.0.0",
     "@types/tether-drop": "^1.4.8",
     "@types/tinycolor2": "^1.4.1",
+    "awesome-typescript-loader": "^5.2.1",
+    "react-docgen-typescript-loader": "^3.0.0",
+    "react-docgen-typescript-webpack-plugin": "^1.1.0",
     "react-test-renderer": "^16.7.0",
     "typescript": "^3.2.2"
+  },
+  "resolutions": {
+    "@types/lodash": "4.14.119"
   }
 }

+ 94 - 0
packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx

@@ -0,0 +1,94 @@
+import React from 'react';
+import { ColorPickerProps } from './ColorPicker';
+import tinycolor from 'tinycolor2';
+import { debounce } from 'lodash';
+
+interface ColorInputState {
+  previousColor: string;
+  value: string;
+}
+
+interface ColorInputProps extends ColorPickerProps {
+  style?: React.CSSProperties;
+}
+
+class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
+  constructor(props: ColorInputProps) {
+    super(props);
+    this.state = {
+      previousColor: props.color,
+      value: props.color,
+    };
+
+    this.updateColor = debounce(this.updateColor, 100);
+  }
+
+  static getDerivedStateFromProps(props: ColorPickerProps, state: ColorInputState) {
+    const newColor = tinycolor(props.color);
+    if (newColor.isValid() && props.color !== state.previousColor) {
+      return {
+        ...state,
+        previousColor: props.color,
+        value: newColor.toString(),
+      };
+    }
+
+    return state;
+  }
+  updateColor = (color: string) => {
+    this.props.onChange(color);
+  };
+
+  handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+    const newColor = tinycolor(event.currentTarget.value);
+
+    this.setState({
+      value: event.currentTarget.value,
+    });
+
+    if (newColor.isValid()) {
+      this.updateColor(newColor.toString());
+    }
+  };
+
+  handleBlur = () => {
+    const newColor = tinycolor(this.state.value);
+
+    if (!newColor.isValid()) {
+      this.setState({
+        value: this.props.color,
+      });
+    }
+  };
+
+  render() {
+    const { value } = this.state;
+    return (
+      <div
+        style={{
+          display: 'flex',
+          ...this.props.style,
+        }}
+      >
+        <div
+          style={{
+            background: this.props.color,
+            width: '35px',
+            height: '35px',
+            flexGrow: 0,
+            borderRadius: '3px 0 0 3px',
+          }}
+        />
+        <div
+          style={{
+            flexGrow: 1,
+          }}
+        >
+          <input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
+        </div>
+      </div>
+    );
+  }
+}
+
+export default ColorInput;

+ 63 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx

@@ -0,0 +1,63 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withKnobs, boolean } from '@storybook/addon-knobs';
+import { SeriesColorPicker, ColorPicker } from './ColorPicker';
+import { action } from '@storybook/addon-actions';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+import { getThemeKnob } from '../../utils/storybook/themeKnob';
+
+const getColorPickerKnobs = () => {
+  return {
+    selectedTheme: getThemeKnob(),
+    enableNamedColors: boolean('Enable named colors', false),
+  };
+};
+
+const ColorPickerStories = storiesOf('UI/ColorPicker/Pickers', module);
+
+ColorPickerStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
+
+ColorPickerStories.add('default', () => {
+  const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
+  return (
+    <UseState initialState="#00ff00">
+      {(selectedColor, updateSelectedColor) => {
+        return (
+          <ColorPicker
+            enableNamedColors={enableNamedColors}
+            color={selectedColor}
+            onChange={color => {
+              action('Color changed')(color);
+              updateSelectedColor(color);
+            }}
+            theme={selectedTheme || undefined}
+          />
+        );
+      }}
+    </UseState>
+  );
+});
+
+ColorPickerStories.add('Series color picker', () => {
+  const { selectedTheme, enableNamedColors } = getColorPickerKnobs();
+
+  return (
+    <UseState initialState="#00ff00">
+      {(selectedColor, updateSelectedColor) => {
+        return (
+          <SeriesColorPicker
+            enableNamedColors={enableNamedColors}
+            yaxis={1}
+            onToggleAxis={() => {}}
+            color={selectedColor}
+            onChange={color => updateSelectedColor(color)}
+            theme={selectedTheme || undefined}
+          >
+            <div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
+          </SeriesColorPicker>
+        );
+      }}
+    </UseState>
+  );
+});

+ 105 - 52
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -1,61 +1,114 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import Drop from 'tether-drop';
+import React, { Component, createRef } from 'react';
+import PopperController from '../Tooltip/PopperController';
+import Popper, { RenderPopperArrowFn } from '../Tooltip/Popper';
 import { ColorPickerPopover } from './ColorPickerPopover';
+import { Themeable, GrafanaTheme } from '../../types';
+import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
+import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
+import propDeprecationWarning from '../../utils/propDeprecationWarning';
 
-export interface Props {
+type ColorPickerChangeHandler = (color: string) => void;
+
+export interface ColorPickerProps extends Themeable {
   color: string;
-  onChange: (c: string) => void;
+  onChange: ColorPickerChangeHandler;
+
+  /**
+   * @deprecated Use onChange instead
+   */
+  onColorChange?: ColorPickerChangeHandler;
+  enableNamedColors?: boolean;
+  withArrow?: boolean;
+  children?: JSX.Element;
 }
 
-export class ColorPicker extends React.Component<Props, any> {
-  pickerElem: HTMLElement | null;
-  colorPickerDrop: any;
-
-  openColorPicker = () => {
-    const dropContent = <ColorPickerPopover color={this.props.color} onColorSelect={this.onColorSelect} />;
-
-    const dropContentElem = document.createElement('div');
-    ReactDOM.render(dropContent, dropContentElem);
-
-    const drop = new Drop({
-      target: this.pickerElem as Element,
-      content: dropContentElem,
-      position: 'top center',
-      classes: 'drop-popover',
-      openOn: 'click',
-      hoverCloseDelay: 200,
-      tetherOptions: {
-        constraints: [{ to: 'scrollParent', attachment: 'none both' }],
-        attachment: 'bottom center',
-      },
-    });
-
-    drop.on('close', this.closeColorPicker);
-
-    this.colorPickerDrop = drop;
-    this.colorPickerDrop.open();
-  };
+export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
+  const { onColorChange } = props;
+  if (onColorChange) {
+    propDeprecationWarning(componentName, 'onColorChange', 'onChange');
+  }
+};
 
-  closeColorPicker = () => {
-    setTimeout(() => {
-      if (this.colorPickerDrop && this.colorPickerDrop.tether) {
-        this.colorPickerDrop.destroy();
-      }
-    }, 100);
-  };
+export const colorPickerFactory = <T extends ColorPickerProps>(
+  popover: React.ComponentType<T>,
+  displayName = 'ColorPicker',
+  renderPopoverArrowFunction?: RenderPopperArrowFn
+) => {
+  return class ColorPicker extends Component<T, any> {
+    static displayName = displayName;
+    pickerTriggerRef = createRef<HTMLDivElement>();
+
+    handleColorChange = (color: string) => {
+      const { onColorChange, onChange } = this.props;
+      const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
 
-  onColorSelect = (color: string) => {
-    this.props.onChange(color);
+      return changeHandler(color);
+    };
+
+    render() {
+      const popoverElement = React.createElement(popover, {
+        ...this.props,
+        onChange: this.handleColorChange,
+      });
+      const { theme, withArrow, children } = this.props;
+
+      const renderArrow: RenderPopperArrowFn = ({ arrowProps, placement }) => {
+        return (
+          <div
+            {...arrowProps}
+            data-placement={placement}
+            className={`ColorPicker__arrow ColorPicker__arrow--${theme === GrafanaTheme.Light ? 'light' : 'dark'}`}
+          />
+        );
+      };
+
+      return (
+        <PopperController content={popoverElement} hideAfter={300}>
+          {(showPopper, hidePopper, popperProps) => {
+            return (
+              <>
+                {this.pickerTriggerRef.current && (
+                  <Popper
+                    {...popperProps}
+                    referenceElement={this.pickerTriggerRef.current}
+                    wrapperClassName="ColorPicker"
+                    renderArrow={withArrow && (renderPopoverArrowFunction || renderArrow)}
+                    onMouseLeave={hidePopper}
+                    onMouseEnter={showPopper}
+                  />
+                )}
+
+                {children ? (
+                  React.cloneElement(children as JSX.Element, {
+                    ref: this.pickerTriggerRef,
+                    onClick: showPopper,
+                    onMouseLeave: hidePopper,
+                  })
+                ) : (
+                  <div
+                    ref={this.pickerTriggerRef}
+                    onClick={showPopper}
+                    onMouseLeave={hidePopper}
+                    className="sp-replacer sp-light"
+                  >
+                    <div className="sp-preview">
+                      <div
+                        className="sp-preview-inner"
+                        style={{
+                          backgroundColor: getColorFromHexRgbOrName(this.props.color || '#000000', theme),
+                        }}
+                      />
+                    </div>
+                  </div>
+                )}
+              </>
+            );
+          }}
+        </PopperController>
+      );
+    }
   };
+};
 
-  render() {
-    return (
-      <div className="sp-replacer sp-light" onClick={this.openColorPicker} ref={element => (this.pickerElem = element)}>
-        <div className="sp-preview">
-          <div className="sp-preview-inner" style={{ backgroundColor: this.props.color }} />
-        </div>
-      </div>
-    );
-  }
-}
+export const ColorPicker = colorPickerFactory(ColorPickerPopover, 'ColorPicker');
+export const SeriesColorPicker = colorPickerFactory(SeriesColorPickerPopover, 'SeriesColorPicker');

+ 40 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.story.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { ColorPickerPopover } from './ColorPickerPopover';
+import { withKnobs } from '@storybook/addon-knobs';
+
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { getThemeKnob } from '../../utils/storybook/themeKnob';
+import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
+
+const ColorPickerPopoverStories = storiesOf('UI/ColorPicker/Popovers', module);
+
+ColorPickerPopoverStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
+
+ColorPickerPopoverStories.add('default', () => {
+  const selectedTheme = getThemeKnob();
+
+  return (
+    <ColorPickerPopover
+      color="#BC67E6"
+      onChange={color => {
+        console.log(color);
+      }}
+      theme={selectedTheme || undefined}
+    />
+  );
+});
+
+ColorPickerPopoverStories.add('SeriesColorPickerPopover', () => {
+  const selectedTheme = getThemeKnob();
+
+  return (
+    <SeriesColorPickerPopover
+      color="#BC67E6"
+      onChange={color => {
+        console.log(color);
+      }}
+      theme={selectedTheme || undefined}
+    />
+  );
+});

+ 75 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.test.tsx

@@ -0,0 +1,75 @@
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { ColorPickerPopover } from './ColorPickerPopover';
+import { getColorDefinitionByName, getNamedColorPalette } from '../../utils/namedColorsPalette';
+import { ColorSwatch } from './NamedColorsGroup';
+import { flatten } from 'lodash';
+import { GrafanaTheme } from '../../types';
+
+const allColors = flatten(Array.from(getNamedColorPalette().values()));
+
+describe('ColorPickerPopover', () => {
+  const BasicGreen = getColorDefinitionByName('green');
+  const BasicBlue = getColorDefinitionByName('blue');
+
+  describe('rendering', () => {
+    it('should render provided color as selected if color provided by name', () => {
+      const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} />);
+      const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
+      const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
+
+      expect(selectedSwatch.length).toBe(1);
+      expect(notSelectedSwatches.length).toBe(allColors.length - 1);
+      expect(selectedSwatch.prop('isSelected')).toBe(true);
+    });
+
+    it('should render provided color as selected if color provided by hex', () => {
+      const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} />);
+      const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
+      const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
+
+      expect(selectedSwatch.length).toBe(1);
+      expect(notSelectedSwatches.length).toBe(allColors.length - 1);
+      expect(selectedSwatch.prop('isSelected')).toBe(true);
+    });
+  });
+
+  describe('named colors support', () => {
+    const onChangeSpy = jest.fn();
+    let wrapper: ReactWrapper;
+
+    afterEach(() => {
+      wrapper.unmount();
+      onChangeSpy.mockClear();
+    });
+
+    it('should pass hex color value to onChange prop by default', () => {
+      wrapper = mount(
+        <ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={GrafanaTheme.Light} />
+      );
+      const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
+
+      basicBlueSwatch.simulate('click');
+
+      expect(onChangeSpy).toBeCalledTimes(1);
+      expect(onChangeSpy).toBeCalledWith(BasicBlue.variants.light);
+    });
+
+    it('should pass color name to onChange prop when named colors enabled', () => {
+      wrapper = mount(
+        <ColorPickerPopover
+          enableNamedColors
+          color={BasicGreen.variants.dark}
+          onChange={onChangeSpy}
+          theme={GrafanaTheme.Light}
+        />
+      );
+      const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
+
+      basicBlueSwatch.simulate('click');
+
+      expect(onChangeSpy).toBeCalledTimes(1);
+      expect(onChangeSpy).toBeCalledWith(BasicBlue.name);
+    });
+  });
+});

+ 102 - 85
packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx

@@ -1,112 +1,129 @@
 import React from 'react';
-import $ from 'jquery';
-import tinycolor from 'tinycolor2';
-import { ColorPalette } from './ColorPalette';
-import { SpectrumPicker } from './SpectrumPicker';
+import { NamedColorsPalette } from './NamedColorsPalette';
+import { getColorName, getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
+import { ColorPickerProps, warnAboutColorPickerPropsDeprecation } from './ColorPicker';
+import { GrafanaTheme } from '../../types';
+import { PopperContentProps } from '../Tooltip/PopperController';
+import SpectrumPalette from './SpectrumPalette';
 
-const DEFAULT_COLOR = '#000000';
-
-export interface Props {
-  color: string;
-  onColorSelect: (c: string) => void;
+export interface Props<T> extends ColorPickerProps, PopperContentProps {
+  customPickers?: T;
 }
 
-export class ColorPickerPopover extends React.Component<Props, any> {
-  pickerNavElem: any;
+type PickerType = 'palette' | 'spectrum';
+
+interface CustomPickersDescriptor {
+  [key: string]: {
+    tabComponent: React.ComponentType<ColorPickerProps>;
+    name: string;
+  };
+}
+interface State<T> {
+  activePicker: PickerType | keyof T;
+}
 
-  constructor(props: Props) {
+export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React.Component<Props<T>, State<T>> {
+  constructor(props: Props<T>) {
     super(props);
     this.state = {
-      tab: 'palette',
-      color: this.props.color || DEFAULT_COLOR,
-      colorString: this.props.color || DEFAULT_COLOR,
+      activePicker: 'palette',
     };
+    warnAboutColorPickerPropsDeprecation('ColorPickerPopover', props);
   }
 
-  setPickerNavElem(elem: any) {
-    this.pickerNavElem = $(elem);
-  }
+  getTabClassName = (tabName: PickerType | keyof T) => {
+    const { activePicker } = this.state;
+    return `ColorPickerPopover__tab ${activePicker === tabName && 'ColorPickerPopover__tab--active'}`;
+  };
 
-  setColor(color: string) {
-    const newColor = tinycolor(color);
-    if (newColor.isValid()) {
-      this.setState({ color: newColor.toString(), colorString: newColor.toString() });
-      this.props.onColorSelect(color);
-    }
-  }
+  handleChange = (color: any) => {
+    const { onColorChange, onChange, enableNamedColors, theme } = this.props;
+    const changeHandler = onColorChange || onChange;
 
-  sampleColorSelected(color: string) {
-    this.setColor(color);
-  }
+    if (enableNamedColors) {
+      return changeHandler(color);
+    }
+    changeHandler(getColorFromHexRgbOrName(color, theme));
+  };
 
-  spectrumColorSelected(color: any) {
-    const rgbColor = color.toRgbString();
-    this.setColor(rgbColor);
-  }
+  handleTabChange = (tab: PickerType | keyof T) => {
+    return () => this.setState({ activePicker: tab });
+  };
 
-  onColorStringChange(e: any) {
-    const colorString = e.target.value;
-    this.setState({ colorString: colorString });
+  renderPicker = () => {
+    const { activePicker } = this.state;
+    const { color, theme } = this.props;
 
-    const newColor = tinycolor(colorString);
-    if (newColor.isValid()) {
-      // Update only color state
-      const newColorString = newColor.toString();
-      this.setState({ color: newColorString });
-      this.props.onColorSelect(newColorString);
+    switch (activePicker) {
+      case 'spectrum':
+        return <SpectrumPalette color={color} onChange={this.handleChange} theme={theme} />;
+      case 'palette':
+        return <NamedColorsPalette color={getColorName(color, theme)} onChange={this.handleChange} theme={theme} />;
+      default:
+        return this.renderCustomPicker(activePicker);
     }
-  }
+  };
 
-  onColorStringBlur(e: any) {
-    const colorString = e.target.value;
-    this.setColor(colorString);
-  }
+  renderCustomPicker = (tabKey: keyof T) => {
+    const { customPickers, color, theme } = this.props;
+    if (!customPickers) {
+      return null;
+    }
 
-  componentDidMount() {
-    this.pickerNavElem.find('li:first').addClass('active');
-    this.pickerNavElem.on('show', (e: any) => {
-      // use href attr (#name => name)
-      const tab = e.target.hash.slice(1);
-      this.setState({ tab: tab });
+    return React.createElement(customPickers[tabKey].tabComponent, {
+      color,
+      theme,
+      onChange: this.handleChange,
     });
-  }
+  };
 
-  render() {
-    const paletteTab = (
-      <div id="palette">
-        <ColorPalette color={this.state.color} onColorSelect={this.sampleColorSelected.bind(this)} />
-      </div>
-    );
-    const spectrumTab = (
-      <div id="spectrum">
-        <SpectrumPicker color={this.state.color} onColorSelect={this.spectrumColorSelected.bind(this)} options={{}} />
-      </div>
+  renderCustomPickerTabs = () => {
+    const { customPickers } = this.props;
+
+    if (!customPickers) {
+      return null;
+    }
+
+    return (
+      <>
+        {Object.keys(customPickers).map(key => {
+          return (
+            <div
+              className={this.getTabClassName(key)}
+              onClick={this.handleTabChange(key)}
+              key={key}
+            >
+              {customPickers[key].name}
+            </div>
+          );
+        })}
+      </>
     );
-    const currentTab = this.state.tab === 'palette' ? paletteTab : spectrumTab;
+  };
+
+  render() {
+    const { theme } = this.props;
+    const colorPickerTheme = theme || GrafanaTheme.Dark;
 
     return (
-      <div className="gf-color-picker">
-        <ul className="nav nav-tabs" id="colorpickernav" ref={this.setPickerNavElem.bind(this)}>
-          <li className="gf-tabs-item-colorpicker">
-            <a href="#palette" data-toggle="tab">
-              Colors
-            </a>
-          </li>
-          <li className="gf-tabs-item-colorpicker">
-            <a href="#spectrum" data-toggle="tab">
-              Custom
-            </a>
-          </li>
-        </ul>
-        <div className="gf-color-picker__body">{currentTab}</div>
-        <div>
-          <input
-            className="gf-form-input gf-form-input--small"
-            value={this.state.colorString}
-            onChange={this.onColorStringChange.bind(this)}
-            onBlur={this.onColorStringBlur.bind(this)}
-          />
+      <div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
+        <div className="ColorPickerPopover__tabs">
+          <div
+            className={this.getTabClassName('palette')}
+            onClick={this.handleTabChange('palette')}
+          >
+            Colors
+          </div>
+          <div
+            className={this.getTabClassName('spectrum')}
+            onClick={this.handleTabChange('spectrum')}
+          >
+            Custom
+          </div>
+          {this.renderCustomPickerTabs()}
         </div>
+
+        <div className="ColorPickerPopover__content">{this.renderPicker()}</div>
       </div>
     );
   }

+ 110 - 0
packages/grafana-ui/src/components/ColorPicker/NamedColorsGroup.tsx

@@ -0,0 +1,110 @@
+import React, { FunctionComponent } from 'react';
+import { Themeable, GrafanaTheme } from '../../types';
+import { ColorDefinition, getColorForTheme } from '../../utils/namedColorsPalette';
+import { Color } from 'csstype';
+import { find, upperFirst } from 'lodash';
+
+type ColorChangeHandler = (color: ColorDefinition) => void;
+
+export enum ColorSwatchVariant {
+  Small = 'small',
+  Large = 'large',
+}
+
+interface ColorSwatchProps extends Themeable, React.DOMAttributes<HTMLDivElement> {
+  color: string;
+  label?: string;
+  variant?: ColorSwatchVariant;
+  isSelected?: boolean;
+}
+
+export const ColorSwatch: FunctionComponent<ColorSwatchProps> = ({
+  color,
+  label,
+  variant = ColorSwatchVariant.Small,
+  isSelected,
+  theme,
+  ...otherProps
+}) => {
+  const isSmall = variant === ColorSwatchVariant.Small;
+  const swatchSize = isSmall ? '16px' : '32px';
+  const selectedSwatchBorder = theme === GrafanaTheme.Light ? '#ffffff' : '#1A1B1F';
+  const swatchStyles = {
+    width: swatchSize,
+    height: swatchSize,
+    borderRadius: '50%',
+    background: `${color}`,
+    marginRight: isSmall ? '0px' : '8px',
+    boxShadow: isSelected ? `inset 0 0 0 2px ${color}, inset 0 0 0 4px ${selectedSwatchBorder}` : 'none',
+  };
+
+  return (
+    <div
+      style={{
+        display: 'flex',
+        alignItems: 'center',
+        cursor: 'pointer',
+      }}
+      {...otherProps}
+    >
+      <div style={swatchStyles} />
+      {variant === ColorSwatchVariant.Large && <span>{label}</span>}
+    </div>
+  );
+};
+
+interface NamedColorsGroupProps extends Themeable {
+  colors: ColorDefinition[];
+  selectedColor?: Color;
+  onColorSelect: ColorChangeHandler;
+  key?: string;
+}
+
+const NamedColorsGroup: FunctionComponent<NamedColorsGroupProps> = ({
+  colors,
+  selectedColor,
+  onColorSelect,
+  theme,
+  ...otherProps
+}) => {
+  const primaryColor = find(colors, color => !!color.isPrimary);
+
+  return (
+    <div {...otherProps} style={{ display: 'flex', flexDirection: 'column' }}>
+      {primaryColor && (
+        <ColorSwatch
+          key={primaryColor.name}
+          isSelected={primaryColor.name === selectedColor}
+          variant={ColorSwatchVariant.Large}
+          color={getColorForTheme(primaryColor, theme)}
+          label={upperFirst(primaryColor.hue)}
+          onClick={() => onColorSelect(primaryColor)}
+          theme={theme}
+        />
+      )}
+      <div
+        style={{
+          display: 'flex',
+          marginTop: '8px',
+        }}
+      >
+        {colors.map(
+          color =>
+            !color.isPrimary && (
+              <div key={color.name} style={{ marginRight: '4px' }}>
+                <ColorSwatch
+                  key={color.name}
+                  isSelected={color.name === selectedColor}
+                  color={getColorForTheme(color, theme)}
+                  onClick={() => onColorSelect(color)}
+                  theme={theme}
+                />
+              </div>
+            )
+        )}
+      </div>
+    </div>
+  );
+};
+
+export default NamedColorsGroup;

+ 52 - 0
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { NamedColorsPalette } from './NamedColorsPalette';
+import { getColorName, getColorDefinitionByName } from '../../utils/namedColorsPalette';
+import { withKnobs, select } from '@storybook/addon-knobs';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+
+const BasicGreen = getColorDefinitionByName('green');
+const BasicBlue = getColorDefinitionByName('blue');
+const LightBlue = getColorDefinitionByName('light-blue');
+
+const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
+
+NamedColorsPaletteStories.addDecorator(withKnobs).addDecorator(withCenteredStory);
+
+NamedColorsPaletteStories.add('Named colors swatch - support for named colors', () => {
+  const selectedColor = select(
+    'Selected color',
+    {
+      Green: 'green',
+      Red: 'red',
+      'Light blue': 'light-blue',
+    },
+    'red'
+  );
+
+  return (
+    <UseState initialState={selectedColor}>
+      {(selectedColor, updateSelectedColor) => {
+        return <NamedColorsPalette color={selectedColor} onChange={updateSelectedColor} />;
+      }}
+    </UseState>
+  );
+}).add('Named colors swatch - support for hex values', () => {
+  const selectedColor = select(
+    'Selected color',
+    {
+      Green: BasicGreen.variants.dark,
+      Red: BasicBlue.variants.dark,
+      'Light blue': LightBlue.variants.dark,
+    },
+    'red'
+  );
+  return (
+    <UseState initialState={selectedColor}>
+      {(selectedColor, updateSelectedColor) => {
+        return <NamedColorsPalette color={getColorName(selectedColor)} onChange={updateSelectedColor} />;
+      }}
+    </UseState>
+  );
+});

+ 36 - 0
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.test.tsx

@@ -0,0 +1,36 @@
+import React from 'react';
+import { mount, ReactWrapper } from 'enzyme';
+import { NamedColorsPalette } from './NamedColorsPalette';
+import { ColorSwatch } from './NamedColorsGroup';
+import { getColorDefinitionByName } from '../../utils';
+import { GrafanaTheme } from '../../types';
+
+describe('NamedColorsPalette', () => {
+
+  const BasicGreen = getColorDefinitionByName('green');
+
+  describe('theme support for named colors', () => {
+    let wrapper: ReactWrapper, selectedSwatch;
+
+    afterEach(() => {
+      wrapper.unmount();
+    });
+
+    it('should render provided color variant specific for theme', () => {
+      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Dark} onChange={() => {}} />);
+      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
+      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
+
+      wrapper.unmount();
+      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={GrafanaTheme.Light} onChange={() => {}} />);
+      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
+      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
+    });
+
+    it('should render dar variant of provided color when theme not provided', () => {
+      wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} />);
+      selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
+      expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
+    });
+  });
+});

+ 39 - 0
packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.tsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import { Color, getNamedColorPalette } from '../../utils/namedColorsPalette';
+import { Themeable } from '../../types/index';
+import NamedColorsGroup from './NamedColorsGroup';
+
+interface NamedColorsPaletteProps extends Themeable {
+  color?: Color;
+  onChange: (colorName: string) => void;
+}
+
+export const NamedColorsPalette = ({ color, onChange, theme }: NamedColorsPaletteProps) => {
+  const swatches: JSX.Element[] = [];
+  getNamedColorPalette().forEach((colors, hue) => {
+    swatches.push(
+      <NamedColorsGroup
+        key={hue}
+        theme={theme}
+        selectedColor={color}
+        colors={colors}
+        onColorSelect={color => {
+          onChange(color.name);
+        }}
+      />
+    );
+  });
+
+  return (
+    <div
+      style={{
+        display: 'grid',
+        gridTemplateColumns: 'repeat(3, 1fr)',
+        gridRowGap: '24px',
+        gridColumnGap: '24px',
+      }}
+    >
+      {swatches}
+    </div>
+  );
+};

+ 0 - 85
packages/grafana-ui/src/components/ColorPicker/SeriesColorPicker.tsx

@@ -1,85 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import Drop from 'tether-drop';
-import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
-
-export interface SeriesColorPickerProps {
-  color: string;
-  yaxis?: number;
-  optionalClass?: string;
-  onColorChange: (newColor: string) => void;
-  onToggleAxis?: () => void;
-}
-
-export class SeriesColorPicker extends React.Component<SeriesColorPickerProps> {
-  pickerElem: any;
-  colorPickerDrop: any;
-
-  static defaultProps = {
-    optionalClass: '',
-    yaxis: undefined,
-    onToggleAxis: () => {},
-  };
-
-  constructor(props: SeriesColorPickerProps) {
-    super(props);
-  }
-
-  componentWillUnmount() {
-    this.destroyDrop();
-  }
-
-  onClickToOpen = () => {
-    if (this.colorPickerDrop) {
-      this.destroyDrop();
-    }
-
-    const { color, yaxis, onColorChange, onToggleAxis } = this.props;
-    const dropContent = (
-      <SeriesColorPickerPopover color={color} yaxis={yaxis} onColorChange={onColorChange} onToggleAxis={onToggleAxis} />
-    );
-    const dropContentElem = document.createElement('div');
-    ReactDOM.render(dropContent, dropContentElem);
-
-    const drop = new Drop({
-      target: this.pickerElem,
-      content: dropContentElem,
-      position: 'bottom center',
-      classes: 'drop-popover',
-      openOn: 'hover',
-      hoverCloseDelay: 200,
-      remove: true,
-      tetherOptions: {
-        constraints: [{ to: 'scrollParent', attachment: 'none both' }],
-        attachment: 'bottom center',
-      },
-    });
-
-    drop.on('close', this.closeColorPicker.bind(this));
-
-    this.colorPickerDrop = drop;
-    this.colorPickerDrop.open();
-  };
-
-  closeColorPicker() {
-    setTimeout(() => {
-      this.destroyDrop();
-    }, 100);
-  }
-
-  destroyDrop() {
-    if (this.colorPickerDrop && this.colorPickerDrop.tether) {
-      this.colorPickerDrop.destroy();
-      this.colorPickerDrop = null;
-    }
-  }
-
-  render() {
-    const { optionalClass, children } = this.props;
-    return (
-      <div className={optionalClass} ref={e => (this.pickerElem = e)} onClick={this.onClickToOpen}>
-        {children}
-      </div>
-    );
-  }
-}

+ 35 - 14
packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx

@@ -1,23 +1,44 @@
-import React from 'react';
+import React, { FunctionComponent } from 'react';
+
 import { ColorPickerPopover } from './ColorPickerPopover';
+import { ColorPickerProps } from './ColorPicker';
+import { PopperContentProps } from '../Tooltip/PopperController';
+import { Switch } from '../Switch/Switch';
 
-export interface SeriesColorPickerPopoverProps {
-  color: string;
+export interface SeriesColorPickerPopoverProps extends ColorPickerProps, PopperContentProps {
   yaxis?: number;
-  onColorChange: (color: string) => void;
   onToggleAxis?: () => void;
 }
 
-export class SeriesColorPickerPopover extends React.PureComponent<SeriesColorPickerPopoverProps, any> {
-  render() {
-    return (
-      <div className="graph-legend-popover">
-        {this.props.yaxis && <AxisSelector yaxis={this.props.yaxis} onToggleAxis={this.props.onToggleAxis} />}
-        <ColorPickerPopover color={this.props.color} onColorSelect={this.props.onColorChange} />
-      </div>
-    );
-  }
-}
+export const SeriesColorPickerPopover: FunctionComponent<SeriesColorPickerPopoverProps> = props => {
+  const { yaxis, onToggleAxis, color, ...colorPickerProps } = props;
+
+  return (
+    <ColorPickerPopover
+      {...colorPickerProps}
+      color={color || '#000000'}
+      customPickers={{
+        yaxis: {
+          name: 'Y-Axis',
+          tabComponent: () => (
+            <Switch
+              key="yaxisSwitch"
+              label="Use right y-axis"
+              className="ColorPicker__axisSwitch"
+              labelClass="ColorPicker__axisSwitchLabel"
+              checked={yaxis === 2}
+              onChange={() => {
+                if (onToggleAxis) {
+                  onToggleAxis();
+                }
+              }}
+            />
+          ),
+        },
+      }}
+    />
+  );
+};
 
 interface AxisSelectorProps {
   yaxis: number;

+ 23 - 0
packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.story.tsx

@@ -0,0 +1,23 @@
+import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { withKnobs } from '@storybook/addon-knobs';
+
+import SpectrumPalette from './SpectrumPalette';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { UseState } from '../../utils/storybook/UseState';
+import { getThemeKnob } from '../../utils/storybook/themeKnob';
+
+const SpectrumPaletteStories = storiesOf('UI/ColorPicker/Palettes/SpectrumPalette', module);
+
+SpectrumPaletteStories.addDecorator(withCenteredStory).addDecorator(withKnobs);
+
+SpectrumPaletteStories.add('Named colors swatch - support for named colors', () => {
+  const selectedTheme = getThemeKnob();
+  return (
+    <UseState initialState="red">
+      {(selectedColor, updateSelectedColor) => {
+        return <SpectrumPalette theme={selectedTheme} color={selectedColor} onChange={updateSelectedColor} />;
+      }}
+    </UseState>
+  );
+});

+ 100 - 0
packages/grafana-ui/src/components/ColorPicker/SpectrumPalette.tsx

@@ -0,0 +1,100 @@
+import React from 'react';
+import { CustomPicker, ColorResult } from 'react-color';
+
+import { Saturation, Hue, Alpha } from 'react-color/lib/components/common';
+import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
+import tinycolor from 'tinycolor2';
+import ColorInput from './ColorInput';
+import { Themeable, GrafanaTheme } from '../../types';
+import SpectrumPalettePointer, { SpectrumPalettePointerProps } from './SpectrumPalettePointer';
+
+export interface SpectrumPaletteProps extends Themeable {
+  color: string;
+  onChange: (color: string) => void;
+}
+
+const renderPointer = (theme?: GrafanaTheme) => (props: SpectrumPalettePointerProps) => (
+  <SpectrumPalettePointer {...props} theme={theme} />
+);
+
+// @ts-ignore
+const SpectrumPicker = CustomPicker<Themeable>(({ rgb, hsl, onChange, theme }) => {
+  return (
+    <div
+      style={{
+        display: 'flex',
+        width: '100%',
+        flexDirection: 'column',
+      }}
+    >
+      <div
+        style={{
+          display: 'flex',
+        }}
+      >
+        <div
+          style={{
+            display: 'flex',
+            flexGrow: 1,
+            flexDirection: 'column',
+          }}
+        >
+          <div
+            style={{
+              position: 'relative',
+              height: '100px',
+              width: '100%',
+            }}
+          >
+            {/*
+      // @ts-ignore */}
+            <Saturation onChange={onChange} hsl={hsl} hsv={tinycolor(hsl).toHsv()} />
+          </div>
+          <div
+            style={{
+              width: '100%',
+              height: '16px',
+              marginTop: '16px',
+              position: 'relative',
+              background: 'white',
+            }}
+          >
+            {/*
+      // @ts-ignore */}
+            <Alpha rgb={rgb} hsl={hsl} a={rgb.a} onChange={onChange} pointer={renderPointer(theme)} />
+          </div>
+        </div>
+
+        <div
+          style={{
+            position: 'relative',
+            width: '16px',
+            height: '100px',
+            marginLeft: '16px',
+          }}
+        >
+          {/*
+        // @ts-ignore */}
+          <Hue onChange={onChange} hsl={hsl} direction="vertical" pointer={renderPointer(theme)} />
+        </div>
+      </div>
+    </div>
+  );
+});
+
+const SpectrumPalette: React.FunctionComponent<SpectrumPaletteProps> = ({ color, onChange, theme }) => {
+  return (
+    <div>
+      <SpectrumPicker
+        color={tinycolor(getColorFromHexRgbOrName(color)).toRgb()}
+        onChange={(a: ColorResult) => {
+          onChange(tinycolor(a.rgb).toString());
+        }}
+        theme={theme}
+      />
+      <ColorInput color={color} onChange={onChange} style={{ marginTop: '16px' }} />
+    </div>
+  );
+};
+
+export default SpectrumPalette;

+ 80 - 0
packages/grafana-ui/src/components/ColorPicker/SpectrumPalettePointer.tsx

@@ -0,0 +1,80 @@
+import React from 'react';
+import { GrafanaTheme, Themeable } from '../../types';
+
+export interface SpectrumPalettePointerProps extends Themeable {
+  direction?: string;
+}
+
+const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProps> = ({
+  theme,
+  direction,
+}) => {
+  const styles = {
+    picker: {
+      width: '16px',
+      height: '16px',
+      transform: direction === 'vertical' ? 'translate(0, -8px)' : 'translate(-8px, 0)',
+    },
+  };
+
+  const pointerColor = theme === GrafanaTheme.Light ? '#3F444D' : '#8E8E8E';
+
+  let pointerStyles: React.CSSProperties = {
+    position: 'absolute',
+    left: '6px',
+    width: '0',
+    height: '0',
+    borderStyle: 'solid',
+    background: 'none',
+  };
+
+  let topArrowStyles: React.CSSProperties = {
+    top: '-7px',
+    borderWidth: '6px 3px 0px 3px',
+    borderColor: `${pointerColor} transparent transparent transparent`,
+  };
+
+  let bottomArrowStyles: React.CSSProperties = {
+    bottom: '-7px',
+    borderWidth: '0px 3px 6px 3px',
+    borderColor: ` transparent transparent ${pointerColor} transparent`,
+  };
+
+  if (direction === 'vertical') {
+    pointerStyles = {
+      ...pointerStyles,
+      left: 'auto',
+    };
+    topArrowStyles = {
+      borderWidth: '3px 0px 3px 6px',
+      borderColor: `transparent transparent transparent ${pointerColor}`,
+      left: '-7px',
+      top: '7px',
+    };
+    bottomArrowStyles = {
+      borderWidth: '3px 6px 3px 0px',
+      borderColor: `transparent ${pointerColor} transparent transparent`,
+      right: '-7px',
+      top: '7px',
+    };
+  }
+
+  return (
+    <div style={styles.picker}>
+      <div
+        style={{
+          ...pointerStyles,
+          ...topArrowStyles,
+        }}
+      />
+      <div
+        style={{
+          ...pointerStyles,
+          ...bottomArrowStyles,
+        }}
+      />
+    </div>
+  );
+};
+
+export default SpectrumPalettePointer;

+ 0 - 72
packages/grafana-ui/src/components/ColorPicker/SpectrumPicker.tsx

@@ -1,72 +0,0 @@
-import React from 'react';
-import _ from 'lodash';
-import $ from 'jquery';
-import '../../vendor/spectrum';
-
-export interface Props {
-  color: string;
-  options: object;
-  onColorSelect: (c: string) => void;
-}
-
-export class SpectrumPicker extends React.Component<Props, any> {
-  elem: any;
-  isMoving: boolean;
-
-  constructor(props: Props) {
-    super(props);
-    this.onSpectrumMove = this.onSpectrumMove.bind(this);
-    this.setComponentElem = this.setComponentElem.bind(this);
-  }
-
-  setComponentElem(elem: any) {
-    this.elem = $(elem);
-  }
-
-  onSpectrumMove(color: any) {
-    this.isMoving = true;
-    this.props.onColorSelect(color);
-  }
-
-  componentDidMount() {
-    const spectrumOptions = _.assignIn(
-      {
-        flat: true,
-        showAlpha: true,
-        showButtons: false,
-        color: this.props.color,
-        appendTo: this.elem,
-        move: this.onSpectrumMove,
-      },
-      this.props.options
-    );
-
-    this.elem.spectrum(spectrumOptions);
-    this.elem.spectrum('show');
-    this.elem.spectrum('set', this.props.color);
-  }
-
-  componentWillUpdate(nextProps: any) {
-    // If user move pointer over spectrum field this produce 'move' event and component
-    // may update props.color. We don't want to update spectrum color in this case, so we can use
-    // isMoving flag for tracking moving state. Flag should be cleared in componentDidUpdate() which
-    // is called after updating occurs (when user finished moving).
-    if (!this.isMoving) {
-      this.elem.spectrum('set', nextProps.color);
-    }
-  }
-
-  componentDidUpdate() {
-    if (this.isMoving) {
-      this.isMoving = false;
-    }
-  }
-
-  componentWillUnmount() {
-    this.elem.spectrum('destroy');
-  }
-
-  render() {
-    return <div className="spectrum-container" ref={this.setComponentElem} />;
-  }
-}

+ 192 - 1
packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss

@@ -1,8 +1,172 @@
+$arrowSize: 15px;
+.ColorPicker {
+  @extend .popper;
+  font-size: 12px;
+}
+
+.ColorPicker__arrow {
+  width: 0;
+  height: 0;
+  border-style: solid;
+  position: absolute;
+  margin: 0px;
+
+  &[data-placement^='top'] {
+    border-width: $arrowSize $arrowSize 0 $arrowSize;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    border-bottom-color: transparent;
+    bottom: -$arrowSize;
+    left: calc(50%-#{$arrowSize});
+    padding-top: $arrowSize;
+  }
+
+  &[data-placement^='bottom'] {
+    border-width: 0 $arrowSize $arrowSize $arrowSize;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    top: 0;
+    left: calc(50%-#{$arrowSize});
+  }
+
+  &[data-placement^='bottom-start'] {
+    border-width: 0 $arrowSize $arrowSize $arrowSize;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    top: 0;
+    left: $arrowSize;
+  }
+
+  &[data-placement^='bottom-end'] & {
+    border-width: 0 $arrowSize $arrowSize $arrowSize;
+    border-left-color: transparent;
+    border-right-color: transparent;
+    border-top-color: transparent;
+    top: 0;
+    left: calc(100% -$arrowSize);
+  }
+
+  &[data-placement^='right'] {
+    border-width: $arrowSize $arrowSize $arrowSize 0;
+    border-left-color: transparent;
+    border-top-color: transparent;
+    border-bottom-color: transparent;
+    left: 0;
+    top: calc(50%-#{$arrowSize});
+  }
+
+  &[data-placement^='left'] {
+    border-width: $arrowSize 0 $arrowSize $arrowSize;
+    border-top-color: transparent;
+    border-right-color: transparent;
+    border-bottom-color: transparent;
+    right: -$arrowSize;
+    top: calc(50%-#{$arrowSize});
+  }
+}
+
+.ColorPicker__arrow--light {
+  border-color: #ffffff;
+}
+
+.ColorPicker__arrow--dark {
+  border-color: #1e2028;
+}
+
+// Top
+.ColorPicker[data-placement^='top'] {
+  padding-bottom: $arrowSize;
+}
+
+// Bottom
+.ColorPicker[data-placement^='bottom'] {
+  padding-top: $arrowSize;
+}
+
+.ColorPicker[data-placement^='bottom-start'] {
+  padding-top: $arrowSize;
+}
+
+.ColorPicker[data-placement^='bottom-end'] {
+  padding-top: $arrowSize;
+}
+
+// Right
+.ColorPicker[data-placement^='right'] {
+  padding-left: $arrowSize;
+}
+
+// Left
+.ColorPicker[data-placement^='left'] {
+  padding-right: $arrowSize;
+}
+
+.ColorPickerPopover {
+  border-radius: 3px;
+}
+
+.ColorPickerPopover--light {
+  color: black;
+  background: linear-gradient(180deg, #ffffff 0%, #f7f8fa 104.25%);
+  box-shadow: 0px 2px 4px #dde4ed, 0px 0px 2px #dde4ed;
+}
+
+.ColorPickerPopover--dark {
+  color: #d8d9da;
+  background: linear-gradient(180deg, #1e2028 0%, #161719 104.25%);
+  box-shadow: 0px 2px 4px #000000, 0px 0px 2px #000000;
+
+  .ColorPickerPopover__tab {
+    background: #303133;
+    color: white;
+    cursor: pointer;
+  }
+  .ColorPickerPopover__tab--active {
+    background: none;
+  }
+}
+
+.ColorPickerPopover__content {
+  width: 336px;
+  min-height: 184px;
+  padding: 24px;
+}
+
+.ColorPickerPopover__tabs {
+  display: flex;
+  width: 100%;
+  border-radius: 3px 3px 0 0;
+  overflow: hidden;
+}
+
+.ColorPickerPopover__tab {
+  width: 50%;
+  text-align: center;
+  padding: 8px 0;
+  background: #dde4ed;
+}
+
+.ColorPickerPopover__tab--active {
+  background: white;
+}
+
+.ColorPicker__axisSwitch {
+  width: 100%;
+}
+
+.ColorPicker__axisSwitchLabel {
+  display: flex;
+  flex-grow: 1;
+}
+
 .sp-replacer {
   background: inherit;
   border: none;
   color: inherit;
   padding: 0;
+  border-radius: 10px;
 }
 
 .sp-replacer:hover,
@@ -35,10 +199,22 @@
   margin: 0;
   float: left;
   z-index: 0;
+  background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
+}
+
+.sp-preview-inner,
+.sp-alpha-inner,
+.sp-thumb-inner {
+  display: block;
+  position: absolute;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
 }
 
 .gf-color-picker__body {
-  padding-bottom: 10px;
+  padding-bottom: $arrowSize;
   padding-left: 6px;
 }
 
@@ -47,3 +223,18 @@
     width: 210px;
   }
 }
+
+// TODO: Remove. This is a temporary solution until color picker popovers are used
+// with Drop.js.
+.drop-popover.drop-popover--transparent {
+  .drop-content {
+    border: none;
+    background: none;
+    padding: 0;
+    max-width: none;
+
+    &:before {
+      display: none;
+    }
+  }
+}

+ 29 - 11
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -7,10 +7,12 @@ interface Props {
   autoHide?: boolean;
   autoHideTimeout?: number;
   autoHideDuration?: number;
-  autoMaxHeight?: string;
+  autoHeightMax?: string;
   hideTracksWhenNotNeeded?: boolean;
+  renderTrackHorizontal?: React.FunctionComponent<any>;
+  renderTrackVertical?: React.FunctionComponent<any>;
   scrollTop?: number;
-  setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
+  setScrollTop: (event: any) => void;
   autoHeightMin?: number | string;
 }
 
@@ -20,13 +22,13 @@ interface Props {
 export class CustomScrollbar extends PureComponent<Props> {
   static defaultProps: Partial<Props> = {
     customClassName: 'custom-scrollbars',
-    autoHide: true,
+    autoHide: false,
     autoHideTimeout: 200,
     autoHideDuration: 200,
-    autoMaxHeight: '100%',
-    hideTracksWhenNotNeeded: false,
     setScrollTop: () => {},
-    autoHeightMin: '0'
+    hideTracksWhenNotNeeded: false,
+    autoHeightMin: '0',
+    autoHeightMax: '100%',
   };
 
   private ref: React.RefObject<Scrollbars>;
@@ -45,7 +47,7 @@ export class CustomScrollbar extends PureComponent<Props> {
       } else {
         ref.scrollTop(this.props.scrollTop);
       }
-   }
+    }
   }
 
   componentDidMount() {
@@ -57,18 +59,34 @@ export class CustomScrollbar extends PureComponent<Props> {
   }
 
   render() {
-    const { customClassName, children, autoMaxHeight } = this.props;
+    const {
+      customClassName,
+      children,
+      autoHeightMax,
+      autoHeightMin,
+      setScrollTop,
+      autoHide,
+      autoHideTimeout,
+      hideTracksWhenNotNeeded,
+      renderTrackHorizontal,
+      renderTrackVertical,
+    } = this.props;
 
     return (
       <Scrollbars
         ref={this.ref}
         className={customClassName}
+        onScroll={setScrollTop}
         autoHeight={true}
+        autoHide={autoHide}
+        autoHideTimeout={autoHideTimeout}
+        hideTracksWhenNotNeeded={hideTracksWhenNotNeeded}
         // These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
         // Before these where set to inhert but that caused problems with cut of legends in firefox
-        autoHeightMax={autoMaxHeight}
-        renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
-        renderTrackVertical={props => <div {...props} className="track-vertical" />}
+        autoHeightMax={autoHeightMax}
+        autoHeightMin={autoHeightMin}
+        renderTrackHorizontal={renderTrackHorizontal || (props => <div {...props} className="track-horizontal" />)}
+        renderTrackVertical={renderTrackVertical || (props => <div {...props} className="track-vertical" />)}
         renderThumbHorizontal={props => <div {...props} className="thumb-horizontal" />}
         renderThumbVertical={props => <div {...props} className="thumb-vertical" />}
         renderView={props => <div {...props} className="view" />}

+ 2 - 2
packages/grafana-ui/src/components/CustomScrollbar/__snapshots__/CustomScrollbar.test.tsx.snap

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

+ 24 - 0
packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx

@@ -0,0 +1,24 @@
+import React, { FunctionComponent } from 'react';
+import { storiesOf } from '@storybook/react';
+import { DeleteButton } from '@grafana/ui';
+
+const CenteredStory: FunctionComponent<{}> = ({ children }) => {
+  return (
+    <div
+      style={{
+        height: '100vh  ',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+storiesOf('UI/DeleteButton', module)
+  .addDecorator(story => <CenteredStory>{story()}</CenteredStory>)
+  .add('default', () => {
+    return <DeleteButton onConfirm={() => {}} />;
+  });

+ 17 - 13
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -1,10 +1,14 @@
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
 
-import { ValueMapping, Threshold, ThemeName, BasicGaugeColor, ThemeNames } from '../../types/panel';
+import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
 import { TimeSeriesVMs } from '../../types/series';
+import { GrafanaTheme } from '../../types';
+import { getMappedValue } from '../../utils/valueMappings';
 import { getValueFormat } from '../../utils/valueFormats/valueFormats';
-import { TimeSeriesValue, getMappedValue } from '../../utils/valueMappings';
+import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
+
+type TimeSeriesValue = string | number | null;
 
 export interface Props {
   decimals: number;
@@ -21,7 +25,7 @@ export interface Props {
   suffix: string;
   unit: string;
   width: number;
-  theme?: ThemeName;
+  theme?: GrafanaTheme;
 }
 
 export class Gauge extends PureComponent<Props> {
@@ -38,7 +42,7 @@ export class Gauge extends PureComponent<Props> {
     thresholds: [],
     unit: 'none',
     stat: 'avg',
-    theme: ThemeNames.Dark,
+    theme: GrafanaTheme.Dark,
   };
 
   componentDidMount() {
@@ -71,29 +75,29 @@ export class Gauge extends PureComponent<Props> {
   }
 
   getFontColor(value: TimeSeriesValue) {
-    const { thresholds } = this.props;
+    const { thresholds, theme } = this.props;
 
     if (thresholds.length === 1) {
-      return thresholds[0].color;
+      return getColorFromHexRgbOrName(thresholds[0].color, theme);
     }
 
     const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
     if (atThreshold) {
-      return atThreshold.color;
+      return getColorFromHexRgbOrName(atThreshold.color, theme);
     }
 
     const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
 
     if (belowThreshold.length > 0) {
       const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
-      return nearestThreshold.color;
+      return getColorFromHexRgbOrName(nearestThreshold.color, theme);
     }
 
     return BasicGaugeColor.Red;
   }
 
   getFormattedThresholds() {
-    const { maxValue, minValue, thresholds } = this.props;
+    const { maxValue, minValue, thresholds, theme } = this.props;
 
     const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index);
     const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1];
@@ -101,13 +105,13 @@ export class Gauge extends PureComponent<Props> {
     const formattedThresholds = [
       ...thresholdsSortedByIndex.map(threshold => {
         if (threshold.index === 0) {
-          return { value: minValue, color: threshold.color };
+          return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme) };
         }
 
         const previousThreshold = thresholdsSortedByIndex[threshold.index - 1];
-        return { value: threshold.value, color: previousThreshold.color };
+        return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme) };
       }),
-      { value: maxValue, color: lastThreshold.color },
+      { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme) },
     ];
 
     return formattedThresholds;
@@ -135,7 +139,7 @@ export class Gauge extends PureComponent<Props> {
     }
 
     const dimension = Math.min(width, height * 1.3);
-    const backgroundColor = theme === ThemeNames.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
+    const backgroundColor = theme === GrafanaTheme.Light ? 'rgb(230,230,230)' : 'rgb(38,38,38)';
     const fontScale = parseInt('80', 10) / 100;
     const fontSize = Math.min(dimension / 5, 100) * fontScale;
     const gaugeWidthReduceRatio = showThresholdLabels ? 1.5 : 1;

+ 1 - 1
packages/grafana-ui/src/components/Select/Select.tsx

@@ -61,7 +61,7 @@ interface AsyncProps {
 export const MenuList = (props: any) => {
   return (
     <components.MenuList {...props}>
-      <CustomScrollbar autoHide={false} autoMaxHeight="inherit">{props.children}</CustomScrollbar>
+      <CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
     </components.MenuList>
   );
 };

+ 1 - 0
packages/grafana-ui/src/components/Select/SelectOption.test.tsx

@@ -3,6 +3,7 @@ import renderer from 'react-test-renderer';
 import SelectOption from './SelectOption';
 import { OptionProps } from 'react-select/lib/components/Option';
 
+// @ts-ignore
 const model: OptionProps<any> = {
   data: jest.fn(),
   cx: jest.fn(),

+ 7 - 5
public/app/core/components/Switch/Switch.tsx → packages/grafana-ui/src/components/Switch/Switch.tsx

@@ -4,10 +4,11 @@ import _ from 'lodash';
 export interface Props {
   label: string;
   checked: boolean;
+  className?: string;
   labelClass?: string;
   switchClass?: string;
   transparent?: boolean;
-  onChange: (event) => any;
+  onChange: (event?: React.SyntheticEvent<HTMLInputElement>) => void;
 }
 
 export interface State {
@@ -19,20 +20,21 @@ export class Switch extends PureComponent<Props, State> {
     id: _.uniqueId(),
   };
 
-  internalOnChange = event => {
+  internalOnChange = (event: React.FormEvent<HTMLInputElement>) => {
     event.stopPropagation();
-    this.props.onChange(event);
+
+    this.props.onChange();
   };
 
   render() {
-    const { labelClass = '', switchClass = '', label, checked, transparent } = this.props;
+    const { labelClass = '', switchClass = '', label, checked, transparent, className } = this.props;
 
     const labelId = `check-${this.state.id}`;
     const labelClassName = `gf-form-label ${labelClass} ${transparent ? 'gf-form-label--transparent' : ''} pointer`;
     const switchClassName = `gf-form-switch ${switchClass} ${transparent ? 'gf-form-switch--transparent' : ''}`;
 
     return (
-      <label htmlFor={labelId} className="gf-form gf-form-switch-container">
+      <label htmlFor={labelId} className={`gf-form gf-form-switch-container ${className}`}>
         {label && <div className={labelClassName}>{label}</div>}
         <div className={switchClassName}>
           <input id={labelId} type="checkbox" checked={checked} onChange={this.internalOnChange} />

+ 8 - 5
packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx

@@ -1,12 +1,11 @@
 import React, { PureComponent } from 'react';
-// import tinycolor, { ColorInput } from 'tinycolor2';
-
-import { Threshold } from '../../types';
+import { Threshold, Themeable } from '../../types';
 import { ColorPicker } from '../ColorPicker/ColorPicker';
 import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
 import { colors } from '../../utils';
+import { getColorFromHexRgbOrName } from '@grafana/ui';
 
-export interface Props {
+export interface Props extends Themeable {
   thresholds: Threshold[];
   onChange: (thresholds: Threshold[]) => void;
 }
@@ -189,6 +188,7 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
 
   render() {
     const { thresholds } = this.state;
+    const { theme } = this.props;
 
     return (
       <PanelOptionsGroup title="Thresholds">
@@ -199,7 +199,10 @@ export class ThresholdsEditor extends PureComponent<Props, State> {
                 <div className="thresholds-row-add-button" onClick={() => this.onAddThreshold(threshold.index + 1)}>
                   <i className="fa fa-plus" />
                 </div>
-                <div className="thresholds-row-color-indicator" style={{ backgroundColor: threshold.color }} />
+                <div
+                  className="thresholds-row-color-indicator"
+                  style={{ backgroundColor: getColorFromHexRgbOrName(threshold.color, theme) }}
+                />
                 <div className="thresholds-row-input">{this.renderInput(threshold)}</div>
               </div>
             );

+ 58 - 43
packages/grafana-ui/src/components/Tooltip/Popper.tsx

@@ -1,73 +1,88 @@
 import React, { PureComponent } from 'react';
 import * as PopperJS from 'popper.js';
-import { Manager, Popper as ReactPopper } from 'react-popper';
+import { Manager, Popper as ReactPopper, PopperArrowProps } from 'react-popper';
 import { Portal } from '@grafana/ui';
 import Transition from 'react-transition-group/Transition';
-
-export enum Themes {
-  Default = 'popper__background--default',
-  Error = 'popper__background--error',
-  Brand = 'popper__background--brand',
-}
+import { PopperContent } from './PopperController';
 
 const defaultTransitionStyles = {
   transition: 'opacity 200ms linear',
   opacity: 0,
 };
 
-const transitionStyles: {[key: string]: object} = {
+const transitionStyles: { [key: string]: object } = {
   exited: { opacity: 0 },
   entering: { opacity: 0 },
-  entered: { opacity: 1 },
-  exiting: { opacity: 0 },
+  entered: { opacity: 1, transitionDelay: '0s' },
+  exiting: { opacity: 0, transitionDelay: '500ms' },
 };
 
-interface Props extends React.DOMAttributes<HTMLDivElement> {
-  renderContent: (content: any) => any;
+export type RenderPopperArrowFn = (
+  props: {
+    arrowProps: PopperArrowProps;
+    placement: string;
+  }
+) => JSX.Element;
+
+interface Props extends React.HTMLAttributes<HTMLDivElement> {
   show: boolean;
   placement?: PopperJS.Placement;
-  content: string | ((props: any) => JSX.Element);
+  content: PopperContent<any>;
   referenceElement: PopperJS.ReferenceObject;
-  theme?: Themes;
+  wrapperClassName?: string;
+  renderArrow?: RenderPopperArrowFn;
 }
 
 class Popper extends PureComponent<Props> {
   render() {
-    const { renderContent, show, placement, onMouseEnter, onMouseLeave, theme } = this.props;
+    const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props;
     const { content } = this.props;
 
-    const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
-
     return (
       <Manager>
         <Transition in={show} timeout={100} mountOnEnter={true} unmountOnExit={true}>
-          {transitionState => (
-            <Portal>
-              <ReactPopper placement={placement} referenceElement={this.props.referenceElement}>
-                {({ ref, style, placement, arrowProps }) => {
-                  return (
-                    <div
-                      onMouseEnter={onMouseEnter}
-                      onMouseLeave={onMouseLeave}
-                      ref={ref}
-                      style={{
-                        ...style,
-                        ...defaultTransitionStyles,
-                        ...transitionStyles[transitionState],
-                      }}
-                      data-placement={placement}
-                      className="popper"
-                    >
-                      <div className={popperBackgroundClassName}>
-                        {renderContent(content)}
-                        <div ref={arrowProps.ref} data-placement={placement} className="popper__arrow" />
+          {transitionState => {
+            return (
+              <Portal>
+                <ReactPopper
+                  placement={placement}
+                  referenceElement={this.props.referenceElement}
+                  // TODO: move modifiers config to popper controller
+                  modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
+                >
+                  {({ ref, style, placement, arrowProps, scheduleUpdate }) => {
+                    return (
+                      <div
+                        onMouseEnter={onMouseEnter}
+                        onMouseLeave={onMouseLeave}
+                        ref={ref}
+                        style={{
+                          ...style,
+                          ...defaultTransitionStyles,
+                          ...transitionStyles[transitionState],
+                        }}
+                        data-placement={placement}
+                        className={`${wrapperClassName}`}
+                      >
+                        <div className={className}>
+                          {typeof content === 'string'
+                            ? content
+                            : React.cloneElement(content, {
+                                updatePopperPosition: scheduleUpdate,
+                              })}
+                          {renderArrow &&
+                            renderArrow({
+                              arrowProps,
+                              placement,
+                            })}
+                        </div>
                       </div>
-                    </div>
-                  );
-                }}
-              </ReactPopper>
-            </Portal>
-          )}
+                    );
+                  }}
+                </ReactPopper>
+              </Portal>
+            );
+          }}
         </Transition>
       </Manager>
     );

+ 27 - 22
packages/grafana-ui/src/components/Tooltip/PopperController.tsx

@@ -1,16 +1,19 @@
 import React from 'react';
 import * as PopperJS from 'popper.js';
-import { Themes } from './Popper';
 
-type PopperContent = string | (() => JSX.Element);
+// This API allows popovers to update Popper's position when e.g. popover content changes
+// updatePopperPosition is delivered to content by react-popper
+export interface PopperContentProps {
+  updatePopperPosition?: () => void;
+}
+
+export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
 
 export interface UsingPopperProps {
   show?: boolean;
   placement?: PopperJS.Placement;
-  content: PopperContent;
+  content: PopperContent<any>;
   children: JSX.Element;
-  renderContent?: (content: PopperContent) => JSX.Element;
-  theme?: Themes;
 }
 
 type PopperControllerRenderProp = (
@@ -19,18 +22,16 @@ type PopperControllerRenderProp = (
   popperProps: {
     show: boolean;
     placement: PopperJS.Placement;
-    content: string | ((props: any) => JSX.Element);
-    renderContent: (content: any) => any;
-    theme?: Themes;
+    content: PopperContent<any>;
   }
 ) => JSX.Element;
 
 interface Props {
   placement?: PopperJS.Placement;
-  content: PopperContent;
+  content: PopperContent<any>;
   className?: string;
   children: PopperControllerRenderProp;
-  theme?: Themes;
+  hideAfter?: number;
 }
 
 interface State {
@@ -39,6 +40,8 @@ interface State {
 }
 
 class PopperController extends React.Component<Props, State> {
+  private hideTimeout: any;
+
   constructor(props: Props) {
     super(props);
 
@@ -60,6 +63,10 @@ class PopperController extends React.Component<Props, State> {
   }
 
   showPopper = () => {
+    if (this.hideTimeout) {
+      clearTimeout(this.hideTimeout);
+    }
+
     this.setState(prevState => ({
       ...prevState,
       show: true,
@@ -67,31 +74,29 @@ class PopperController extends React.Component<Props, State> {
   };
 
   hidePopper = () => {
+    if (this.props.hideAfter !== 0) {
+      this.hideTimeout = setTimeout(() => {
+        this.setState(prevState => ({
+          ...prevState,
+          show: false,
+        }));
+      }, this.props.hideAfter);
+      return;
+    }
     this.setState(prevState => ({
       ...prevState,
       show: false,
     }));
   };
 
-  renderContent(content: PopperContent) {
-    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 { children, content, theme } = this.props;
+    const { children, content } = this.props;
     const { show, placement } = this.state;
 
     return children(this.showPopper, this.hidePopper, {
       show,
       placement,
       content,
-      renderContent: this.renderContent,
-      theme,
     });
   }
 }

+ 16 - 1
packages/grafana-ui/src/components/Tooltip/Tooltip.tsx

@@ -3,8 +3,18 @@ import * as PopperJS from 'popper.js';
 import Popper from './Popper';
 import PopperController, { UsingPopperProps } from './PopperController';
 
-export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPopperProps) => {
+export enum Themes {
+  Default = 'popper__background--default',
+  Error = 'popper__background--error',
+  Brand = 'popper__background--brand',
+}
+
+interface TooltipProps extends UsingPopperProps {
+  theme?: Themes;
+}
+export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) => {
   const tooltipTriggerRef = createRef<PopperJS.ReferenceObject>();
+  const popperBackgroundClassName = 'popper__background' + (theme ? ' ' + theme : '');
 
   return (
     <PopperController {...controllerProps}>
@@ -17,6 +27,11 @@ export const Tooltip = ({ children, renderContent, ...controllerProps }: UsingPo
                 onMouseEnter={showPopper}
                 onMouseLeave={hidePopper}
                 referenceElement={tooltipTriggerRef.current}
+                wrapperClassName='popper'
+                className={popperBackgroundClassName}
+                renderArrow={({ arrowProps, placement }) => (
+                  <div className="popper__arrow" data-placement={placement} {...arrowProps} />
+                )}
               />
             )}
             {React.cloneElement(children, {

+ 4 - 5
packages/grafana-ui/src/components/Tooltip/_Tooltip.scss

@@ -1,6 +1,5 @@
 $popper-margin-from-ref: 5px;
 
-
 @mixin popper-theme($backgroundColor, $arrowColor) {
   background: $backgroundColor;
   .popper__arrow {
@@ -22,6 +21,10 @@ $popper-margin-from-ref: 5px;
   box-shadow: 0 0 2px rgba(0, 0, 0, 0.5);
   padding: 10px;
 
+  .popper__arrow {
+    border-color: $tooltipBackground;
+  }
+
   // Themes
   &.popper__background--error {
     @include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
@@ -41,10 +44,6 @@ $popper-margin-from-ref: 5px;
   margin: 0px;
 }
 
-.popper__arrow {
-  border-color: $tooltipBackground;
-}
-
 // Top
 .popper[data-placement^='top'] {
   padding-bottom: $popper-margin-from-ref;

+ 2 - 2
packages/grafana-ui/src/components/index.ts

@@ -14,12 +14,12 @@ export { FormLabel } from './FormLabel/FormLabel';
 export { FormField } from './FormField/FormField';
 
 export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
-export { ColorPicker } from './ColorPicker/ColorPicker';
+export {  ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
 export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
-export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
 export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
 export { Graph } from './Graph/Graph';
 export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
 export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
 export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';
 export { Gauge } from './Gauge/Gauge';
+export { Switch } from './Switch/Switch';

+ 0 - 2
packages/grafana-ui/src/index.scss

@@ -1,3 +1 @@
-@import 'vendor/spectrum';
 @import 'components/index';
-

+ 9 - 0
packages/grafana-ui/src/types/index.ts

@@ -3,3 +3,12 @@ export * from './time';
 export * from './panel';
 export * from './plugin';
 export * from './datasource';
+
+export enum GrafanaTheme {
+  Light = 'light',
+  Dark = 'dark',
+}
+
+export interface Themeable {
+  theme?: GrafanaTheme;
+}

+ 1 - 8
packages/grafana-ui/src/types/panel.ts

@@ -36,7 +36,7 @@ export interface PanelMenuItem {
 export interface Threshold {
   index: number;
   value: number;
-  color?: string;
+  color: string;
 }
 
 export enum BasicGaugeColor {
@@ -66,10 +66,3 @@ export interface RangeMap extends BaseMap {
   from: string;
   to: string;
 }
-
-export type ThemeName = 'dark' | 'light';
-
-export enum ThemeNames {
-  Dark = 'dark',
-  Light = 'light',
-}

+ 0 - 1
packages/grafana-ui/src/utils/colors.ts

@@ -9,7 +9,6 @@ export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)';
 export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)';
 export const PENDING_COLOR = 'rgba(247, 149, 32, 1)';
 export const REGION_FILL_ALPHA = 0.09;
-
 export const colors = [
   '#7EB26D', // 0: pale green
   '#EAB839', // 1: mustard

+ 1 - 0
packages/grafana-ui/src/utils/index.ts

@@ -1,3 +1,4 @@
 export * from './processTimeSeries';
 export * from './valueFormats/valueFormats';
 export * from './colors';
+export * from './namedColorsPalette';

+ 66 - 0
packages/grafana-ui/src/utils/namedColorsPalette.test.ts

@@ -0,0 +1,66 @@
+import {
+  getColorName,
+  getColorDefinition,
+  getColorByName,
+  getColorFromHexRgbOrName,
+  getColorDefinitionByName,
+} from './namedColorsPalette';
+import { GrafanaTheme } from '../types/index';
+
+describe('colors', () => {
+  const SemiDarkBlue = getColorDefinitionByName('semi-dark-blue');
+
+  describe('getColorDefinition', () => {
+    it('returns undefined for unknown hex', () => {
+      expect(getColorDefinition('#ff0000', GrafanaTheme.Light)).toBeUndefined();
+      expect(getColorDefinition('#ff0000', GrafanaTheme.Dark)).toBeUndefined();
+    });
+
+    it('returns definition for known hex', () => {
+      expect(getColorDefinition(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue);
+      expect(getColorDefinition(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue);
+    });
+  });
+
+  describe('getColorName', () => {
+    it('returns undefined for unknown hex', () => {
+      expect(getColorName('#ff0000')).toBeUndefined();
+    });
+
+    it('returns name for known hex', () => {
+      expect(getColorName(SemiDarkBlue.variants.light, GrafanaTheme.Light)).toEqual(SemiDarkBlue.name);
+      expect(getColorName(SemiDarkBlue.variants.dark, GrafanaTheme.Dark)).toEqual(SemiDarkBlue.name);
+    });
+  });
+
+  describe('getColorByName', () => {
+    it('returns undefined for unknown color', () => {
+      expect(getColorByName('aruba-sunshine')).toBeUndefined();
+    });
+
+    it('returns color definiton for known color', () => {
+      expect(getColorByName(SemiDarkBlue.name)).toBe(SemiDarkBlue);
+    });
+  });
+
+  describe('getColorFromHexRgbOrName', () => {
+    it('returns undefined for unknown color', () => {
+      expect(() => getColorFromHexRgbOrName('aruba-sunshine')).toThrow();
+    });
+
+    it('returns dark hex variant for known color if theme not specified', () => {
+      expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark);
+    });
+
+    it("returns correct variant's hex for known color if theme specified", () => {
+      expect(getColorFromHexRgbOrName(SemiDarkBlue.name, GrafanaTheme.Light)).toBe(SemiDarkBlue.variants.light);
+    });
+
+    it('returns color if specified as hex or rgb/a', () => {
+      expect(getColorFromHexRgbOrName('ff0000')).toBe('ff0000');
+      expect(getColorFromHexRgbOrName('#ff0000')).toBe('#ff0000');
+      expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)');
+      expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)');
+    });
+  });
+});

+ 182 - 0
packages/grafana-ui/src/utils/namedColorsPalette.ts

@@ -0,0 +1,182 @@
+import { flatten } from 'lodash';
+import { GrafanaTheme } from '../types';
+
+type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple';
+
+export type Color =
+  | 'green'
+  | 'dark-green'
+  | 'semi-dark-green'
+  | 'light-green'
+  | 'super-light-green'
+  | 'yellow'
+  | 'dark-yellow'
+  | 'semi-dark-yellow'
+  | 'light-yellow'
+  | 'super-light-yellow'
+  | 'red'
+  | 'dark-red'
+  | 'semi-dark-red'
+  | 'light-red'
+  | 'super-light-red'
+  | 'blue'
+  | 'dark-blue'
+  | 'semi-dark-blue'
+  | 'light-blue'
+  | 'super-light-blue'
+  | 'orange'
+  | 'dark-orange'
+  | 'semi-dark-orange'
+  | 'light-orange'
+  | 'super-light-orange'
+  | 'purple'
+  | 'dark-purple'
+  | 'semi-dark-purple'
+  | 'light-purple'
+  | 'super-light-purple';
+
+type ThemeVariants = {
+  dark: string;
+  light: string;
+};
+
+export type ColorDefinition = {
+  hue: Hue;
+  isPrimary?: boolean;
+  name: Color;
+  variants: ThemeVariants;
+};
+
+let colorsPaletteInstance: Map<Hue, ColorDefinition[]>;
+
+const buildColorDefinition = (
+  hue: Hue,
+  name: Color,
+  [light, dark]: string[],
+  isPrimary?: boolean
+): ColorDefinition => ({
+  hue,
+  name,
+  variants: {
+    light,
+    dark,
+  },
+  isPrimary: !!isPrimary,
+});
+
+export const getColorDefinitionByName = (name: Color): ColorDefinition => {
+  return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === name)[0];
+};
+
+export const getColorDefinition = (hex: string, theme: GrafanaTheme): ColorDefinition | undefined => {
+  return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
+};
+
+const isHex = (color: string) => {
+  const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi;
+  return hexRegex.test(color);
+};
+
+export const getColorName = (color?: string, theme?: GrafanaTheme): Color | undefined => {
+  if (!color) {
+    return undefined;
+  }
+
+  if (color.indexOf('rgb') > -1) {
+    return undefined;
+  }
+  if (isHex(color)) {
+    const definition = getColorDefinition(color, theme || GrafanaTheme.Dark);
+    return definition ? definition.name : undefined;
+  }
+
+  return color as Color;
+};
+
+export const getColorByName = (colorName: string) => {
+  const definition = flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === colorName);
+  return definition.length > 0 ? definition[0] : undefined;
+};
+
+export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaTheme): string => {
+  if (color.indexOf('rgb') > -1 || isHex(color)) {
+    return color;
+  }
+
+  const colorDefinition = getColorByName(color);
+
+  if (!colorDefinition) {
+    throw new Error('Unknown color');
+  }
+
+  return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark;
+};
+
+export const getColorForTheme = (color: ColorDefinition, theme?: GrafanaTheme) => {
+  return theme ? color.variants[theme] : color.variants.dark;
+};
+
+const buildNamedColorsPalette = () => {
+  const palette = new Map<Hue, ColorDefinition[]>();
+
+    const BasicGreen = buildColorDefinition('green', 'green', ['#56A64B', '#73BF69'], true);
+  const DarkGreen = buildColorDefinition('green', 'dark-green', ['#19730E', '#37872D']);
+  const SemiDarkGreen = buildColorDefinition('green', 'semi-dark-green', ['#37872D', '#56A64B']);
+  const LightGreen = buildColorDefinition('green', 'light-green', ['#73BF69', '#96D98D']);
+  const SuperLightGreen = buildColorDefinition('green', 'super-light-green', ['#96D98D', '#C8F2C2']);
+
+  const BasicYellow = buildColorDefinition('yellow', 'yellow', ['#F2CC0C', '#FADE2A'], true);
+  const DarkYellow = buildColorDefinition('yellow', 'dark-yellow', ['#CC9D00', '#E0B400']);
+  const SemiDarkYellow = buildColorDefinition('yellow', 'semi-dark-yellow', ['#E0B400', '#F2CC0C']);
+  const LightYellow = buildColorDefinition('yellow', 'light-yellow', ['#FADE2A', '#FFEE52']);
+  const SuperLightYellow = buildColorDefinition('yellow', 'super-light-yellow', ['#FFEE52', '#FFF899']);
+
+  const BasicRed = buildColorDefinition('red', 'red', ['#E02F44', '#F2495C'], true);
+  const DarkRed = buildColorDefinition('red', 'dark-red', ['#AD0317', '#C4162A']);
+  const SemiDarkRed = buildColorDefinition('red', 'semi-dark-red', ['#C4162A', '#E02F44']);
+  const LightRed = buildColorDefinition('red', 'light-red', ['#F2495C', '#FF7383']);
+  const SuperLightRed = buildColorDefinition('red', 'super-light-red', ['#FF7383', '#FFA6B0']);
+
+  const BasicBlue = buildColorDefinition('blue', 'blue', ['#3274D9', '#5794F2'], true);
+  const DarkBlue = buildColorDefinition('blue', 'dark-blue', ['#1250B0', '#1F60C4']);
+  const SemiDarkBlue = buildColorDefinition('blue', 'semi-dark-blue', ['#1F60C4', '#3274D9']);
+  const LightBlue = buildColorDefinition('blue', 'light-blue', ['#5794F2', '#8AB8FF']);
+  const SuperLightBlue = buildColorDefinition('blue', 'super-light-blue', ['#8AB8FF', '#C0D8FF']);
+
+  const BasicOrange = buildColorDefinition('orange', 'orange', ['#FF780A', '#FF9830'], true);
+  const DarkOrange = buildColorDefinition('orange', 'dark-orange', ['#E55400', '#FA6400']);
+  const SemiDarkOrange = buildColorDefinition('orange', 'semi-dark-orange', ['#FA6400', '#FF780A']);
+  const LightOrange = buildColorDefinition('orange', 'light-orange', ['#FF9830', '#FFB357']);
+  const SuperLightOrange = buildColorDefinition('orange', 'super-light-orange', ['#FFB357', '#FFCB7D']);
+
+  const BasicPurple = buildColorDefinition('purple', 'purple', ['#A352CC', '#B877D9'], true);
+  const DarkPurple = buildColorDefinition('purple', 'dark-purple', ['#7C2EA3', '#8F3BB8']);
+  const SemiDarkPurple = buildColorDefinition('purple', 'semi-dark-purple', ['#8F3BB8', '#A352CC']);
+  const LightPurple = buildColorDefinition('purple', 'light-purple', ['#B877D9', '#CA95E5']);
+  const SuperLightPurple = buildColorDefinition('purple', 'super-light-purple', ['#CA95E5', '#DEB6F2']);
+
+  const greens = [BasicGreen, DarkGreen, SemiDarkGreen, LightGreen, SuperLightGreen];
+  const yellows = [BasicYellow, DarkYellow, SemiDarkYellow, LightYellow, SuperLightYellow];
+  const reds = [BasicRed, DarkRed, SemiDarkRed, LightRed, SuperLightRed];
+  const blues = [BasicBlue, DarkBlue, SemiDarkBlue, LightBlue, SuperLightBlue];
+  const oranges = [BasicOrange, DarkOrange, SemiDarkOrange, LightOrange, SuperLightOrange];
+  const purples = [BasicPurple, DarkPurple, SemiDarkPurple, LightPurple, SuperLightPurple];
+
+  palette.set('green', greens);
+  palette.set('yellow', yellows);
+  palette.set('red', reds);
+  palette.set('blue', blues);
+  palette.set('orange', oranges);
+  palette.set('purple', purples);
+
+  return palette;
+};
+
+export const getNamedColorPalette = () => {
+  if (colorsPaletteInstance) {
+    return colorsPaletteInstance;
+  }
+
+  colorsPaletteInstance = buildNamedColorsPalette();
+  return colorsPaletteInstance;
+};

+ 6 - 0
packages/grafana-ui/src/utils/propDeprecationWarning.ts

@@ -0,0 +1,6 @@
+const propDeprecationWarning = (componentName: string, propName: string, newPropName: string) => {
+  const message = `[Deprecation warning] ${componentName}: ${propName} is deprecated. Use ${newPropName} instead`;
+  console.warn(message);
+};
+
+export default propDeprecationWarning;

+ 38 - 0
packages/grafana-ui/src/utils/storybook/UseState.tsx

@@ -0,0 +1,38 @@
+import React from 'react';
+
+interface StateHolderProps<T> {
+  initialState: T;
+  children: (currentState: T, updateState: (nextState: T) => void) => JSX.Element;
+}
+
+export class UseState<T> extends React.Component<StateHolderProps<T>, { value: T; initialState: T }> {
+  constructor(props: StateHolderProps<T>) {
+    super(props);
+    this.state = {
+      value: props.initialState,
+      initialState: props.initialState, // To enable control from knobs
+    };
+  }
+  // @ts-ignore
+  static getDerivedStateFromProps(props: StateHolderProps<{}>, state: { value: any; initialState: any }) {
+    if (props.initialState !== state.initialState) {
+      return {
+        initialState: props.initialState,
+        value: props.initialState,
+      };
+    }
+    return {
+      ...state,
+      value: state.value,
+    };
+  }
+
+  handleStateUpdate = (nextState: T) => {
+    console.log(nextState);
+    this.setState({ value: nextState });
+  };
+
+  render() {
+    return this.props.children(this.state.value, this.handleStateUpdate);
+  }
+}

+ 14 - 0
packages/grafana-ui/src/utils/storybook/themeKnob.ts

@@ -0,0 +1,14 @@
+import { select } from '@storybook/addon-knobs';
+import { GrafanaTheme } from '../../types';
+
+export const getThemeKnob = (defaultTheme: GrafanaTheme = GrafanaTheme.Dark) => {
+  return select(
+    'Theme',
+    {
+      Default: defaultTheme,
+      Light: GrafanaTheme.Light,
+      Dark: GrafanaTheme.Dark,
+    },
+    defaultTheme
+  );
+};

+ 19 - 0
packages/grafana-ui/src/utils/storybook/withCenteredStory.tsx

@@ -0,0 +1,19 @@
+import React from 'react';
+import { RenderFunction } from '@storybook/react';
+
+const CenteredStory: React.FunctionComponent<{}> = ({ children }) => {
+  return (
+    <div
+      style={{
+        height: '100vh  ',
+        display: 'flex',
+        alignItems: 'center',
+        justifyContent: 'center',
+      }}
+    >
+      {children}
+    </div>
+  );
+};
+
+export const withCenteredStory = (story: RenderFunction) => <CenteredStory>{story()}</CenteredStory>;

+ 0 - 509
packages/grafana-ui/src/vendor/spectrum.css

@@ -1,509 +0,0 @@
-/***
-Spectrum Colorpicker v1.3.0
-https://github.com/bgrins/spectrum
-Author: Brian Grinstead
-License: MIT
-***/
-
-.sp-container {
-    position:absolute;
-    top:0;
-    left:0;
-    display:inline-block;
-    *display: inline;
-    *zoom: 1;
-    /* https://github.com/bgrins/spectrum/issues/40 */
-    z-index: 9999994;
-    overflow: hidden;
-}
-.sp-container.sp-flat {
-    position: relative;
-}
-
-/* Fix for * { box-sizing: border-box; } */
-.sp-container,
-.sp-container * {
-    -webkit-box-sizing: content-box;
-       -moz-box-sizing: content-box;
-            box-sizing: content-box;
-}
-
-/* http://ansciath.tumblr.com/post/7347495869/css-aspect-ratio */
-.sp-top {
-  position:relative;
-  width: 100%;
-  display:inline-block;
-}
-.sp-top-inner {
-   position:absolute;
-   top:0;
-   left:0;
-   bottom:0;
-   right:0;
-}
-.sp-color {
-    position: absolute;
-    top:0;
-    left:0;
-    bottom:0;
-    right:20%;
-}
-.sp-hue {
-    position: absolute;
-    top:0;
-    right:0;
-    bottom:0;
-    left:84%;
-    height: 100%;
-}
-
-.sp-clear-enabled .sp-hue {
-    top:33px;
-    height: 77.5%;
-}
-
-.sp-fill {
-    padding-top: 80%;
-}
-.sp-sat, .sp-val {
-    position: absolute;
-    top:0;
-    left:0;
-    right:0;
-    bottom:0;
-}
-
-.sp-alpha-enabled .sp-top {
-    margin-bottom: 18px;
-}
-.sp-alpha-enabled .sp-alpha {
-    display: block;
-}
-.sp-alpha-handle {
-    position:absolute;
-    top:-4px;
-    bottom: -4px;
-    width: 6px;
-    left: 50%;
-    cursor: pointer;
-    border: 1px solid black;
-    background: white;
-    opacity: .8;
-}
-.sp-alpha {
-    display: none;
-    position: absolute;
-    bottom: -14px;
-    right: 0;
-    left: 0;
-    height: 8px;
-}
-.sp-alpha-inner {
-    border: solid 1px #333;
-}
-
-.sp-clear {
-    display: none;
-}
-
-.sp-clear.sp-clear-display {
-    background-position: center;
-}
-
-.sp-clear-enabled .sp-clear {
-    display: block;
-    position:absolute;
-    top:0px;
-    right:0;
-    bottom:0;
-    left:84%;
-    height: 28px;
-}
-
-/* Don't allow text selection */
-.sp-container, .sp-replacer, .sp-preview, .sp-dragger, .sp-slider, .sp-alpha, .sp-clear, .sp-alpha-handle, .sp-container.sp-dragging .sp-input, .sp-container button  {
-    -webkit-user-select:none;
-    -moz-user-select: -moz-none;
-    -o-user-select:none;
-    user-select: none;
-}
-
-.sp-container.sp-input-disabled .sp-input-container {
-    display: none;
-}
-.sp-container.sp-buttons-disabled .sp-button-container {
-    display: none;
-}
-.sp-palette-only .sp-picker-container {
-    display: none;
-}
-.sp-palette-disabled .sp-palette-container {
-    display: none;
-}
-
-.sp-initial-disabled .sp-initial {
-    display: none;
-}
-
-
-/* Gradients for hue, saturation and value instead of images.  Not pretty... but it works */
-.sp-sat {
-    background-image: linear-gradient(to right, #fff, rgba(204, 154, 129, 0));
-    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr=#FFFFFFFF, endColorstr=#00CC9A81)";
-    filter : progid:DXImageTransform.Microsoft.gradient(GradientType = 1, startColorstr='#FFFFFFFF', endColorstr='#00CC9A81');
-}
-.sp-val {
-    background-image: linear-gradient(to top, #000, rgba(204, 154, 129, 0));
-    -ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#00CC9A81, endColorstr=#FF000000)";
-    filter : progid:DXImageTransform.Microsoft.gradient(startColorstr='#00CC9A81', endColorstr='#FF000000');
-}
-
-.sp-hue {
-    background: -moz-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
-    background: -ms-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
-    background: -o-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
-    background: -webkit-gradient(linear, left top, left bottom, from(#ff0000), color-stop(0.17, #ffff00), color-stop(0.33, #00ff00), color-stop(0.5, #00ffff), color-stop(0.67, #0000ff), color-stop(0.83, #ff00ff), to(#ff0000));
-    background: -webkit-linear-gradient(top, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%);
-}
-
-/* IE filters do not support multiple color stops.
-   Generate 6 divs, line them up, and do two color gradients for each.
-   Yes, really.
- */
-.sp-1 {
-    height:17%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0000', endColorstr='#ffff00');
-}
-.sp-2 {
-    height:16%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffff00', endColorstr='#00ff00');
-}
-.sp-3 {
-    height:17%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ff00', endColorstr='#00ffff');
-}
-.sp-4 {
-    height:17%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00ffff', endColorstr='#0000ff');
-}
-.sp-5 {
-    height:16%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#0000ff', endColorstr='#ff00ff');
-}
-.sp-6 {
-    height:17%;
-    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff00ff', endColorstr='#ff0000');
-}
-
-.sp-hidden {
-    display: none !important;
-}
-
-/* Clearfix hack */
-.sp-cf:before, .sp-cf:after { content: ""; display: table; }
-.sp-cf:after { clear: both; }
-.sp-cf { *zoom: 1; }
-
-/* Mobile devices, make hue slider bigger so it is easier to slide */
-@media (max-device-width: 480px) {
-    .sp-color { right: 40%; }
-    .sp-hue { left: 63%; }
-    .sp-fill { padding-top: 60%; }
-}
-.sp-dragger {
-   border-radius: 5px;
-   height: 5px;
-   width: 5px;
-   border: 1px solid #fff;
-   background: #000;
-   cursor: pointer;
-   position:absolute;
-   top:0;
-   left: 0;
-}
-.sp-slider {
-    position: absolute;
-    top:0;
-    cursor:pointer;
-    height: 3px;
-    left: -1px;
-    right: -1px;
-    border: 1px solid #000;
-    background: white;
-    opacity: .8;
-}
-
-/*
-Theme authors:
-Here are the basic themeable display options (colors, fonts, global widths).
-See http://bgrins.github.io/spectrum/themes/ for instructions.
-*/
-
-.sp-container {
-    border-radius: 0;
-    background-color: #ECECEC;
-    border: solid 1px #f0c49B;
-    padding: 0;
-}
-.sp-container, .sp-container button, .sp-container input, .sp-color, .sp-hue, .sp-clear
-{
-    font: normal 12px "Lucida Grande", "Lucida Sans Unicode", "Lucida Sans", Geneva, Verdana, sans-serif;
-    -webkit-box-sizing: border-box;
-    -moz-box-sizing: border-box;
-    -ms-box-sizing: border-box;
-    box-sizing: border-box;
-}
-.sp-top
-{
-    margin-bottom: 3px;
-}
-.sp-color, .sp-hue, .sp-clear
-{
-    border: solid 1px #666;
-}
-
-/* Input */
-.sp-input-container {
-    float:right;
-    width: 100px;
-    margin-bottom: 4px;
-}
-.sp-initial-disabled  .sp-input-container {
-    width: 100%;
-}
-.sp-input {
-   font-size: 12px !important;
-   border: 1px inset;
-   padding: 4px 5px;
-   margin: 0;
-   width: 100%;
-   background:transparent;
-   border-radius: 3px;
-   color: #222;
-}
-.sp-input:focus  {
-    border: 1px solid orange;
-}
-.sp-input.sp-validation-error
-{
-    border: 1px solid red;
-    background: #fdd;
-}
-.sp-picker-container , .sp-palette-container
-{
-    float:left;
-    position: relative;
-    padding: 10px;
-    padding-bottom: 300px;
-    margin-bottom: -290px;
-}
-.sp-picker-container
-{
-    width: 172px;
-    border-left: solid 1px #fff;
-}
-
-/* Palettes */
-.sp-palette-container
-{
-    border-right: solid 1px #ccc;
-}
-
-.sp-palette .sp-thumb-el {
-    display: block;
-    position:relative;
-    float:left;
-    width: 24px;
-    height: 15px;
-    margin: 3px;
-    cursor: pointer;
-    border:solid 2px transparent;
-}
-.sp-palette .sp-thumb-el:hover, .sp-palette .sp-thumb-el.sp-thumb-active {
-    border-color: orange;
-}
-.sp-thumb-el
-{
-    position:relative;
-}
-
-/* Initial */
-.sp-initial
-{
-    float: left;
-    border: solid 1px #333;
-}
-.sp-initial span {
-    width: 30px;
-    height: 25px;
-    border:none;
-    display:block;
-    float:left;
-    margin:0;
-}
-
-.sp-initial .sp-clear-display {
-    background-position: center;
-}
-
-/* Buttons */
-.sp-button-container {
-    float: right;
-}
-
-/* Replacer (the little preview div that shows up instead of the <input>) */
-.sp-replacer {
-    margin:0;
-    overflow:hidden;
-    cursor:pointer;
-    padding: 4px;
-    display:inline-block;
-    *zoom: 1;
-    *display: inline;
-    border: solid 1px #91765d;
-    background: #eee;
-    color: #333;
-    vertical-align: middle;
-}
-.sp-replacer:hover, .sp-replacer.sp-active {
-    border-color: #F0C49B;
-    color: #111;
-}
-.sp-replacer.sp-disabled {
-    cursor:default;
-    border-color: silver;
-    color: silver;
-}
-.sp-dd {
-    padding: 2px 0;
-    height: 16px;
-    line-height: 16px;
-    float:left;
-    font-size:10px;
-}
-.sp-preview
-{
-    position:relative;
-    width:25px;
-    height: 20px;
-    border: solid 1px #222;
-    margin-right: 5px;
-    float:left;
-    z-index: 0;
-}
-
-.sp-palette
-{
-    *width: 220px;
-    max-width: 220px;
-}
-.sp-palette .sp-thumb-el
-{
-    width:16px;
-    height: 16px;
-    margin:2px 1px;
-    border: solid 1px #d0d0d0;
-}
-
-.sp-container
-{
-    padding-bottom:0;
-}
-
-
-/* Buttons: http://hellohappy.org/css3-buttons/ */
-.sp-container button {
-  background-color: #eeeeee;
-  background-image: -webkit-linear-gradient(top, #eeeeee, #cccccc);
-  background-image: -moz-linear-gradient(top, #eeeeee, #cccccc);
-  background-image: -ms-linear-gradient(top, #eeeeee, #cccccc);
-  background-image: -o-linear-gradient(top, #eeeeee, #cccccc);
-  background-image: linear-gradient(to bottom, #eeeeee, #cccccc);
-  border: 1px solid #ccc;
-  border-bottom: 1px solid #bbb;
-  border-radius: 3px;
-  color: #333;
-  font-size: 14px;
-  line-height: 1;
-  padding: 5px 4px;
-  text-align: center;
-  text-shadow: 0 1px 0 #eee;
-  vertical-align: middle;
-}
-.sp-container button:hover {
-    background-color: #dddddd;
-    background-image: -webkit-linear-gradient(top, #dddddd, #bbbbbb);
-    background-image: -moz-linear-gradient(top, #dddddd, #bbbbbb);
-    background-image: -ms-linear-gradient(top, #dddddd, #bbbbbb);
-    background-image: -o-linear-gradient(top, #dddddd, #bbbbbb);
-    background-image: linear-gradient(to bottom, #dddddd, #bbbbbb);
-    border: 1px solid #bbb;
-    border-bottom: 1px solid #999;
-    cursor: pointer;
-    text-shadow: 0 1px 0 #ddd;
-}
-.sp-container button:active {
-    border: 1px solid #aaa;
-    border-bottom: 1px solid #888;
-    -webkit-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-    -moz-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-    -ms-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-    -o-box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-    box-shadow: inset 0 0 5px 2px #aaaaaa, 0 1px 0 0 #eeeeee;
-}
-.sp-cancel
-{
-    font-size: 11px;
-    color: #d93f3f !important;
-    margin:0;
-    padding:2px;
-    margin-right: 5px;
-    vertical-align: middle;
-    text-decoration:none;
-
-}
-.sp-cancel:hover
-{
-    color: #d93f3f !important;
-    text-decoration: underline;
-}
-
-
-.sp-palette span:hover, .sp-palette span.sp-thumb-active
-{
-    border-color: #000;
-}
-
-.sp-preview, .sp-alpha, .sp-thumb-el
-{
-    position:relative;
-    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==);
-}
-.sp-preview-inner, .sp-alpha-inner, .sp-thumb-inner
-{
-    display:block;
-    position:absolute;
-    top:0;left:0;bottom:0;right:0;
-}
-
-.sp-palette .sp-thumb-inner
-{
-    background-position: 50% 50%;
-    background-repeat: no-repeat;
-}
-
-.sp-palette .sp-thumb-light.sp-thumb-active .sp-thumb-inner
-{
-    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAIVJREFUeNpiYBhsgJFMffxAXABlN5JruT4Q3wfi/0DsT64h8UD8HmpIPCWG/KemIfOJCUB+Aoacx6EGBZyHBqI+WsDCwuQ9mhxeg2A210Ntfo8klk9sOMijaURm7yc1UP2RNCMbKE9ODK1HM6iegYLkfx8pligC9lCD7KmRof0ZhjQACDAAceovrtpVBRkAAAAASUVORK5CYII=);
-}
-
-.sp-palette .sp-thumb-dark.sp-thumb-active .sp-thumb-inner
-{
-    background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABIAAAASCAYAAABWzo5XAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAadEVYdFNvZnR3YXJlAFBhaW50Lk5FVCB2My41LjEwMPRyoQAAAMdJREFUOE+tkgsNwzAMRMugEAahEAahEAZhEAqlEAZhEAohEAYh81X2dIm8fKpEspLGvudPOsUYpxE2BIJCroJmEW9qJ+MKaBFhEMNabSy9oIcIPwrB+afvAUFoK4H0tMaQ3XtlrggDhOVVMuT4E5MMG0FBbCEYzjYT7OxLEvIHQLY2zWwQ3D+9luyOQTfKDiFD3iUIfPk8VqrKjgAiSfGFPecrg6HN6m/iBcwiDAo7WiBeawa+Kwh7tZoSCGLMqwlSAzVDhoK+6vH4G0P5wdkAAAAASUVORK5CYII=);
-}
-
-.sp-clear-display {
-    background-repeat:no-repeat;
-    background-position: center;
-    background-image: url(data:image/gif;base64,R0lGODlhFAAUAPcAAAAAAJmZmZ2dnZ6enqKioqOjo6SkpKWlpaampqenp6ioqKmpqaqqqqurq/Hx8fLy8vT09PX19ff39/j4+Pn5+fr6+vv7+wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAP8ALAAAAAAUABQAAAihAP9FoPCvoMGDBy08+EdhQAIJCCMybCDAAYUEARBAlFiQQoMABQhKUJBxY0SPICEYHBnggEmDKAuoPMjS5cGYMxHW3IiT478JJA8M/CjTZ0GgLRekNGpwAsYABHIypcAgQMsITDtWJYBR6NSqMico9cqR6tKfY7GeBCuVwlipDNmefAtTrkSzB1RaIAoXodsABiZAEFB06gIBWC1mLVgBa0AAOw==);
-}

+ 0 - 2317
packages/grafana-ui/src/vendor/spectrum.js

@@ -1,2317 +0,0 @@
-// Spectrum Colorpicker v1.7.0
-// https://github.com/bgrins/spectrum
-// Author: Brian Grinstead
-// License: MIT
-
-(function (factory) {
-    "use strict";
-
-    if (typeof define === 'function' && define.amd) { // AMD
-        define(['jquery'], factory);
-    }
-    else if (typeof exports == "object" && typeof module == "object") { // CommonJS
-        module.exports = factory;
-    }
-    else { // Browser
-        factory(jQuery);
-    }
-})(function($, undefined) {
-    "use strict";
-
-    var defaultOpts = {
-
-        // Callbacks
-        beforeShow: noop,
-        move: noop,
-        change: noop,
-        show: noop,
-        hide: noop,
-
-        // Options
-        color: false,
-        flat: false,
-        showInput: false,
-        allowEmpty: false,
-        showButtons: true,
-        clickoutFiresChange: true,
-        showInitial: false,
-        showPalette: false,
-        showPaletteOnly: false,
-        hideAfterPaletteSelect: false,
-        togglePaletteOnly: false,
-        showSelectionPalette: true,
-        localStorageKey: false,
-        appendTo: "body",
-        maxSelectionSize: 7,
-        cancelText: "cancel",
-        chooseText: "choose",
-        togglePaletteMoreText: "more",
-        togglePaletteLessText: "less",
-        clearText: "Clear Color Selection",
-        noColorSelectedText: "No Color Selected",
-        preferredFormat: false,
-        className: "", // Deprecated - use containerClassName and replacerClassName instead.
-        containerClassName: "",
-        replacerClassName: "",
-        showAlpha: false,
-        theme: "sp-light",
-        palette: [["#ffffff", "#000000", "#ff0000", "#ff8000", "#ffff00", "#008000", "#0000ff", "#4b0082", "#9400d3"]],
-        selectionPalette: [],
-        disabled: false,
-        offset: null
-    },
-    spectrums = [],
-    IE = !!/msie/i.exec( window.navigator.userAgent ),
-    rgbaSupport = (function() {
-        function contains( str, substr ) {
-            return !!~('' + str).indexOf(substr);
-        }
-
-        var elem = document.createElement('div');
-        var style = elem.style;
-        style.cssText = 'background-color:rgba(0,0,0,.5)';
-        return contains(style.backgroundColor, 'rgba') || contains(style.backgroundColor, 'hsla');
-    })(),
-    replaceInput = [
-        "<div class='sp-replacer'>",
-            "<div class='sp-preview'><div class='sp-preview-inner'></div></div>",
-            "<div class='sp-dd'>&#9660;</div>",
-        "</div>"
-    ].join(''),
-    markup = (function () {
-
-        // IE does not support gradients with multiple stops, so we need to simulate
-        //  that for the rainbow slider with 8 divs that each have a single gradient
-        var gradientFix = "";
-        if (IE) {
-            for (var i = 1; i <= 6; i++) {
-                gradientFix += "<div class='sp-" + i + "'></div>";
-            }
-        }
-
-        return [
-            "<div class='sp-container sp-hidden'>",
-                "<div class='sp-palette-container'>",
-                    "<div class='sp-palette sp-thumb sp-cf'></div>",
-                    "<div class='sp-palette-button-container sp-cf'>",
-                        "<button type='button' class='sp-palette-toggle'></button>",
-                    "</div>",
-                "</div>",
-                "<div class='sp-picker-container'>",
-                    "<div class='sp-top sp-cf'>",
-                        "<div class='sp-fill'></div>",
-                        "<div class='sp-top-inner'>",
-                            "<div class='sp-color'>",
-                                "<div class='sp-sat'>",
-                                    "<div class='sp-val'>",
-                                        "<div class='sp-dragger'></div>",
-                                    "</div>",
-                                "</div>",
-                            "</div>",
-                            "<div class='sp-clear sp-clear-display'>",
-                            "</div>",
-                            "<div class='sp-hue'>",
-                                "<div class='sp-slider'></div>",
-                                gradientFix,
-                            "</div>",
-                        "</div>",
-                        "<div class='sp-alpha'><div class='sp-alpha-inner'><div class='sp-alpha-handle'></div></div></div>",
-                    "</div>",
-                    "<div class='sp-input-container sp-cf'>",
-                        "<input class='sp-input' type='text' spellcheck='false'  />",
-                    "</div>",
-                    "<div class='sp-initial sp-thumb sp-cf'></div>",
-                    "<div class='sp-button-container sp-cf'>",
-                        "<a class='sp-cancel' href='#'></a>",
-                        "<button type='button' class='sp-choose'></button>",
-                    "</div>",
-                "</div>",
-            "</div>"
-        ].join("");
-    })();
-
-    function paletteTemplate (p, color, className, opts) {
-        var html = [];
-        for (var i = 0; i < p.length; i++) {
-            var current = p[i];
-            if(current) {
-                var tiny = tinycolor(current);
-                var c = tiny.toHsl().l < 0.5 ? "sp-thumb-el sp-thumb-dark" : "sp-thumb-el sp-thumb-light";
-                c += (tinycolor.equals(color, current)) ? " sp-thumb-active" : "";
-                var formattedString = tiny.toString(opts.preferredFormat || "rgb");
-                var swatchStyle = rgbaSupport ? ("background-color:" + tiny.toRgbString()) : "filter:" + tiny.toFilter();
-                html.push('<span title="' + formattedString + '" data-color="' + tiny.toRgbString() + '" class="' + c + '"><span class="sp-thumb-inner" style="' + swatchStyle + ';" /></span>');
-            } else {
-                var cls = 'sp-clear-display';
-                html.push($('<div />')
-                    .append($('<span data-color="" style="background-color:transparent;" class="' + cls + '"></span>')
-                        .attr('title', opts.noColorSelectedText)
-                    )
-                    .html()
-                );
-            }
-        }
-        return "<div class='sp-cf " + className + "'>" + html.join('') + "</div>";
-    }
-
-    function hideAll() {
-        for (var i = 0; i < spectrums.length; i++) {
-            if (spectrums[i]) {
-                spectrums[i].hide();
-            }
-        }
-    }
-
-    function instanceOptions(o, callbackContext) {
-        var opts = $.extend({}, defaultOpts, o);
-        opts.callbacks = {
-            'move': bind(opts.move, callbackContext),
-            'change': bind(opts.change, callbackContext),
-            'show': bind(opts.show, callbackContext),
-            'hide': bind(opts.hide, callbackContext),
-            'beforeShow': bind(opts.beforeShow, callbackContext)
-        };
-
-        return opts;
-    }
-
-    function spectrum(element, o) {
-
-        var opts = instanceOptions(o, element),
-            flat = opts.flat,
-            showSelectionPalette = opts.showSelectionPalette,
-            localStorageKey = opts.localStorageKey,
-            theme = opts.theme,
-            callbacks = opts.callbacks,
-            resize = throttle(reflow, 10),
-            visible = false,
-            isDragging = false,
-            dragWidth = 0,
-            dragHeight = 0,
-            dragHelperHeight = 0,
-            slideHeight = 0,
-            slideWidth = 0,
-            alphaWidth = 0,
-            alphaSlideHelperWidth = 0,
-            slideHelperHeight = 0,
-            currentHue = 0,
-            currentSaturation = 0,
-            currentValue = 0,
-            currentAlpha = 1,
-            palette = [],
-            paletteArray = [],
-            paletteLookup = {},
-            selectionPalette = opts.selectionPalette.slice(0),
-            maxSelectionSize = opts.maxSelectionSize,
-            draggingClass = "sp-dragging",
-            shiftMovementDirection = null;
-
-        var doc = element.ownerDocument,
-            body = doc.body,
-            boundElement = $(element),
-            disabled = false,
-            container = $(markup, doc).addClass(theme),
-            pickerContainer = container.find(".sp-picker-container"),
-            dragger = container.find(".sp-color"),
-            dragHelper = container.find(".sp-dragger"),
-            slider = container.find(".sp-hue"),
-            slideHelper = container.find(".sp-slider"),
-            alphaSliderInner = container.find(".sp-alpha-inner"),
-            alphaSlider = container.find(".sp-alpha"),
-            alphaSlideHelper = container.find(".sp-alpha-handle"),
-            textInput = container.find(".sp-input"),
-            paletteContainer = container.find(".sp-palette"),
-            initialColorContainer = container.find(".sp-initial"),
-            cancelButton = container.find(".sp-cancel"),
-            clearButton = container.find(".sp-clear"),
-            chooseButton = container.find(".sp-choose"),
-            toggleButton = container.find(".sp-palette-toggle"),
-            isInput = boundElement.is("input"),
-            isInputTypeColor = isInput && boundElement.attr("type") === "color" && inputTypeColorSupport(),
-            shouldReplace = isInput && !flat,
-            replacer = (shouldReplace) ? $(replaceInput).addClass(theme).addClass(opts.className).addClass(opts.replacerClassName) : $([]),
-            offsetElement = (shouldReplace) ? replacer : boundElement,
-            previewElement = replacer.find(".sp-preview-inner"),
-            initialColor = opts.color || (isInput && boundElement.val()),
-            colorOnShow = false,
-            preferredFormat = opts.preferredFormat,
-            currentPreferredFormat = preferredFormat,
-            clickoutFiresChange = !opts.showButtons || opts.clickoutFiresChange,
-            isEmpty = !initialColor,
-            allowEmpty = opts.allowEmpty && !isInputTypeColor;
-
-        function applyOptions() {
-
-            if (opts.showPaletteOnly) {
-                opts.showPalette = true;
-            }
-
-            toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
-
-            if (opts.palette) {
-                palette = opts.palette.slice(0);
-                paletteArray = $.isArray(palette[0]) ? palette : [palette];
-                paletteLookup = {};
-                for (var i = 0; i < paletteArray.length; i++) {
-                    for (var j = 0; j < paletteArray[i].length; j++) {
-                        var rgb = tinycolor(paletteArray[i][j]).toRgbString();
-                        paletteLookup[rgb] = true;
-                    }
-                }
-            }
-
-            container.toggleClass("sp-flat", flat);
-            container.toggleClass("sp-input-disabled", !opts.showInput);
-            container.toggleClass("sp-alpha-enabled", opts.showAlpha);
-            container.toggleClass("sp-clear-enabled", allowEmpty);
-            container.toggleClass("sp-buttons-disabled", !opts.showButtons);
-            container.toggleClass("sp-palette-buttons-disabled", !opts.togglePaletteOnly);
-            container.toggleClass("sp-palette-disabled", !opts.showPalette);
-            container.toggleClass("sp-palette-only", opts.showPaletteOnly);
-            container.toggleClass("sp-initial-disabled", !opts.showInitial);
-            container.addClass(opts.className).addClass(opts.containerClassName);
-
-            reflow();
-        }
-
-        function initialize() {
-
-            if (IE) {
-                container.find("*:not(input)").attr("unselectable", "on");
-            }
-
-            applyOptions();
-
-            if (shouldReplace) {
-                boundElement.after(replacer).hide();
-            }
-
-            if (!allowEmpty) {
-                clearButton.hide();
-            }
-
-            if (flat) {
-                boundElement.after(container).hide();
-            }
-            else {
-
-                var appendTo = opts.appendTo === "parent" ? boundElement.parent() : $(opts.appendTo);
-                if (appendTo.length !== 1) {
-                    appendTo = $("body");
-                }
-
-                appendTo.append(container);
-            }
-
-            updateSelectionPaletteFromStorage();
-
-            offsetElement.bind("click.spectrum touchstart.spectrum", function (e) {
-                if (!disabled) {
-                    toggle();
-                }
-
-                e.stopPropagation();
-
-                if (!$(e.target).is("input")) {
-                    e.preventDefault();
-                }
-            });
-
-            if(boundElement.is(":disabled") || (opts.disabled === true)) {
-                disable();
-            }
-
-            // Prevent clicks from bubbling up to document.  This would cause it to be hidden.
-            container.click(stopPropagation);
-
-            // Handle user typed input
-            textInput.change(setFromTextInput);
-            textInput.bind("paste", function () {
-                setTimeout(setFromTextInput, 1);
-            });
-            textInput.keydown(function (e) { if (e.keyCode == 13) { setFromTextInput(); } });
-
-            cancelButton.text(opts.cancelText);
-            cancelButton.bind("click.spectrum", function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-                revert();
-                hide();
-            });
-
-            clearButton.attr("title", opts.clearText);
-            clearButton.bind("click.spectrum", function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-                isEmpty = true;
-                move();
-
-                if(flat) {
-                    //for the flat style, this is a change event
-                    updateOriginalInput(true);
-                }
-            });
-
-            chooseButton.text(opts.chooseText);
-            chooseButton.bind("click.spectrum", function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-
-                if (IE && textInput.is(":focus")) {
-                    textInput.trigger('change');
-                }
-
-                if (isValid()) {
-                    updateOriginalInput(true);
-                    hide();
-                }
-            });
-
-            toggleButton.text(opts.showPaletteOnly ? opts.togglePaletteMoreText : opts.togglePaletteLessText);
-            toggleButton.bind("click.spectrum", function (e) {
-                e.stopPropagation();
-                e.preventDefault();
-
-                opts.showPaletteOnly = !opts.showPaletteOnly;
-
-                // To make sure the Picker area is drawn on the right, next to the
-                // Palette area (and not below the palette), first move the Palette
-                // to the left to make space for the picker, plus 5px extra.
-                // The 'applyOptions' function puts the whole container back into place
-                // and takes care of the button-text and the sp-palette-only CSS class.
-                if (!opts.showPaletteOnly && !flat) {
-                    container.css('left', '-=' + (pickerContainer.outerWidth(true) + 5));
-                }
-                applyOptions();
-            });
-
-            draggable(alphaSlider, function (dragX, dragY, e) {
-                currentAlpha = (dragX / alphaWidth);
-                isEmpty = false;
-                if (e.shiftKey) {
-                    currentAlpha = Math.round(currentAlpha * 10) / 10;
-                }
-
-                move();
-            }, dragStart, dragStop);
-
-            draggable(slider, function (dragX, dragY) {
-                currentHue = parseFloat(dragY / slideHeight);
-                isEmpty = false;
-                if (!opts.showAlpha) {
-                    currentAlpha = 1;
-                }
-                move();
-            }, dragStart, dragStop);
-
-            draggable(dragger, function (dragX, dragY, e) {
-
-                // shift+drag should snap the movement to either the x or y axis.
-                if (!e.shiftKey) {
-                    shiftMovementDirection = null;
-                }
-                else if (!shiftMovementDirection) {
-                    var oldDragX = currentSaturation * dragWidth;
-                    var oldDragY = dragHeight - (currentValue * dragHeight);
-                    var furtherFromX = Math.abs(dragX - oldDragX) > Math.abs(dragY - oldDragY);
-
-                    shiftMovementDirection = furtherFromX ? "x" : "y";
-                }
-
-                var setSaturation = !shiftMovementDirection || shiftMovementDirection === "x";
-                var setValue = !shiftMovementDirection || shiftMovementDirection === "y";
-
-                if (setSaturation) {
-                    currentSaturation = parseFloat(dragX / dragWidth);
-                }
-                if (setValue) {
-                    currentValue = parseFloat((dragHeight - dragY) / dragHeight);
-                }
-
-                isEmpty = false;
-                if (!opts.showAlpha) {
-                    currentAlpha = 1;
-                }
-
-                move();
-
-            }, dragStart, dragStop);
-
-            if (!!initialColor) {
-                set(initialColor);
-
-                // In case color was black - update the preview UI and set the format
-                // since the set function will not run (default color is black).
-                updateUI();
-                currentPreferredFormat = preferredFormat || tinycolor(initialColor).format;
-
-                addColorToSelectionPalette(initialColor);
-            }
-            else {
-                updateUI();
-            }
-
-            if (flat) {
-                show();
-            }
-
-            function paletteElementClick(e) {
-                if (e.data && e.data.ignore) {
-                    set($(e.target).closest(".sp-thumb-el").data("color"));
-                    move();
-                }
-                else {
-                    set($(e.target).closest(".sp-thumb-el").data("color"));
-                    move();
-                    updateOriginalInput(true);
-                    if (opts.hideAfterPaletteSelect) {
-                      hide();
-                    }
-                }
-
-                return false;
-            }
-
-            var paletteEvent = IE ? "mousedown.spectrum" : "click.spectrum touchstart.spectrum";
-            paletteContainer.delegate(".sp-thumb-el", paletteEvent, paletteElementClick);
-            initialColorContainer.delegate(".sp-thumb-el:nth-child(1)", paletteEvent, { ignore: true }, paletteElementClick);
-        }
-
-        function updateSelectionPaletteFromStorage() {
-
-            if (localStorageKey && window.localStorage) {
-
-                // Migrate old palettes over to new format.  May want to remove this eventually.
-                try {
-                    var oldPalette = window.localStorage[localStorageKey].split(",#");
-                    if (oldPalette.length > 1) {
-                        delete window.localStorage[localStorageKey];
-                        $.each(oldPalette, function(i, c) {
-                             addColorToSelectionPalette(c);
-                        });
-                    }
-                }
-                catch(e) { }
-
-                try {
-                    selectionPalette = window.localStorage[localStorageKey].split(";");
-                }
-                catch (e) { }
-            }
-        }
-
-        function addColorToSelectionPalette(color) {
-            if (showSelectionPalette) {
-                var rgb = tinycolor(color).toRgbString();
-                if (!paletteLookup[rgb] && $.inArray(rgb, selectionPalette) === -1) {
-                    selectionPalette.push(rgb);
-                    while(selectionPalette.length > maxSelectionSize) {
-                        selectionPalette.shift();
-                    }
-                }
-
-                if (localStorageKey && window.localStorage) {
-                    try {
-                        window.localStorage[localStorageKey] = selectionPalette.join(";");
-                    }
-                    catch(e) { }
-                }
-            }
-        }
-
-        function getUniqueSelectionPalette() {
-            var unique = [];
-            if (opts.showPalette) {
-                for (var i = 0; i < selectionPalette.length; i++) {
-                    var rgb = tinycolor(selectionPalette[i]).toRgbString();
-
-                    if (!paletteLookup[rgb]) {
-                        unique.push(selectionPalette[i]);
-                    }
-                }
-            }
-
-            return unique.reverse().slice(0, opts.maxSelectionSize);
-        }
-
-        function drawPalette() {
-
-            var currentColor = get();
-
-            var html = $.map(paletteArray, function (palette, i) {
-                return paletteTemplate(palette, currentColor, "sp-palette-row sp-palette-row-" + i, opts);
-            });
-
-            updateSelectionPaletteFromStorage();
-
-            if (selectionPalette) {
-                html.push(paletteTemplate(getUniqueSelectionPalette(), currentColor, "sp-palette-row sp-palette-row-selection", opts));
-            }
-
-            paletteContainer.html(html.join(""));
-        }
-
-        function drawInitial() {
-            if (opts.showInitial) {
-                var initial = colorOnShow;
-                var current = get();
-                initialColorContainer.html(paletteTemplate([initial, current], current, "sp-palette-row-initial", opts));
-            }
-        }
-
-        function dragStart() {
-            if (dragHeight <= 0 || dragWidth <= 0 || slideHeight <= 0) {
-                reflow();
-            }
-            isDragging = true;
-            container.addClass(draggingClass);
-            shiftMovementDirection = null;
-            boundElement.trigger('dragstart.spectrum', [ get() ]);
-        }
-
-        function dragStop() {
-            isDragging = false;
-            container.removeClass(draggingClass);
-            boundElement.trigger('dragstop.spectrum', [ get() ]);
-        }
-
-        function setFromTextInput() {
-
-            var value = textInput.val();
-
-            if ((value === null || value === "") && allowEmpty) {
-                set(null);
-                updateOriginalInput(true);
-            }
-            else {
-                var tiny = tinycolor(value);
-                if (tiny.isValid()) {
-                    set(tiny);
-                    updateOriginalInput(true);
-                }
-                else {
-                    textInput.addClass("sp-validation-error");
-                }
-            }
-        }
-
-        function toggle() {
-            if (visible) {
-                hide();
-            }
-            else {
-                show();
-            }
-        }
-
-        function show() {
-            var event = $.Event('beforeShow.spectrum');
-
-            if (visible) {
-                reflow();
-                return;
-            }
-
-            boundElement.trigger(event, [ get() ]);
-
-            if (callbacks.beforeShow(get()) === false || event.isDefaultPrevented()) {
-                return;
-            }
-
-            hideAll();
-            visible = true;
-
-            $(doc).bind("keydown.spectrum", onkeydown);
-            $(doc).bind("click.spectrum", clickout);
-            $(window).bind("resize.spectrum", resize);
-            replacer.addClass("sp-active");
-            container.removeClass("sp-hidden");
-
-            reflow();
-            updateUI();
-
-            colorOnShow = get();
-
-            drawInitial();
-            callbacks.show(colorOnShow);
-            boundElement.trigger('show.spectrum', [ colorOnShow ]);
-        }
-
-        function onkeydown(e) {
-            // Close on ESC
-            if (e.keyCode === 27) {
-                hide();
-            }
-        }
-
-        function clickout(e) {
-            // Return on right click.
-            if (e.button == 2) { return; }
-
-            // If a drag event was happening during the mouseup, don't hide
-            // on click.
-            if (isDragging) { return; }
-
-            if (clickoutFiresChange) {
-                updateOriginalInput(true);
-            }
-            else {
-                revert();
-            }
-            hide();
-        }
-
-        function hide() {
-            // Return if hiding is unnecessary
-            if (!visible || flat) { return; }
-            visible = false;
-
-            $(doc).unbind("keydown.spectrum", onkeydown);
-            $(doc).unbind("click.spectrum", clickout);
-            $(window).unbind("resize.spectrum", resize);
-
-            replacer.removeClass("sp-active");
-            container.addClass("sp-hidden");
-
-            callbacks.hide(get());
-            boundElement.trigger('hide.spectrum', [ get() ]);
-        }
-
-        function revert() {
-            set(colorOnShow, true);
-        }
-
-        function set(color, ignoreFormatChange) {
-            if (tinycolor.equals(color, get())) {
-                // Update UI just in case a validation error needs
-                // to be cleared.
-                updateUI();
-                return;
-            }
-
-            var newColor, newHsv;
-            if (!color && allowEmpty) {
-                isEmpty = true;
-            } else {
-                isEmpty = false;
-                newColor = tinycolor(color);
-                newHsv = newColor.toHsv();
-
-                currentHue = (newHsv.h % 360) / 360;
-                currentSaturation = newHsv.s;
-                currentValue = newHsv.v;
-                currentAlpha = newHsv.a;
-            }
-            updateUI();
-
-            if (newColor && newColor.isValid() && !ignoreFormatChange) {
-                currentPreferredFormat = preferredFormat || newColor.getFormat();
-            }
-        }
-
-        function get(opts) {
-            opts = opts || { };
-
-            if (allowEmpty && isEmpty) {
-                return null;
-            }
-
-            return tinycolor.fromRatio({
-                h: currentHue,
-                s: currentSaturation,
-                v: currentValue,
-                a: Math.round(currentAlpha * 100) / 100
-            }, { format: opts.format || currentPreferredFormat });
-        }
-
-        function isValid() {
-            return !textInput.hasClass("sp-validation-error");
-        }
-
-        function move() {
-            updateUI();
-
-            callbacks.move(get());
-            boundElement.trigger('move.spectrum', [ get() ]);
-        }
-
-        function updateUI() {
-
-            textInput.removeClass("sp-validation-error");
-
-            updateHelperLocations();
-
-            // Update dragger background color (gradients take care of saturation and value).
-            var flatColor = tinycolor.fromRatio({ h: currentHue, s: 1, v: 1 });
-            dragger.css("background-color", flatColor.toHexString());
-
-            // Get a format that alpha will be included in (hex and names ignore alpha)
-            var format = currentPreferredFormat;
-            if (currentAlpha < 1 && !(currentAlpha === 0 && format === "name")) {
-                if (format === "hex" || format === "hex3" || format === "hex6" || format === "name") {
-                    format = "rgb";
-                }
-            }
-
-            var realColor = get({ format: format }),
-                displayColor = '';
-
-             //reset background info for preview element
-            previewElement.removeClass("sp-clear-display");
-            previewElement.css('background-color', 'transparent');
-
-            if (!realColor && allowEmpty) {
-                // Update the replaced elements background with icon indicating no color selection
-                previewElement.addClass("sp-clear-display");
-            }
-            else {
-                var realHex = realColor.toHexString(),
-                    realRgb = realColor.toRgbString();
-
-                // Update the replaced elements background color (with actual selected color)
-                if (rgbaSupport || realColor.alpha === 1) {
-                    previewElement.css("background-color", realRgb);
-                }
-                else {
-                    previewElement.css("background-color", "transparent");
-                    previewElement.css("filter", realColor.toFilter());
-                }
-
-                if (opts.showAlpha) {
-                    var rgb = realColor.toRgb();
-                    rgb.a = 0;
-                    var realAlpha = tinycolor(rgb).toRgbString();
-                    var gradient = "linear-gradient(left, " + realAlpha + ", " + realHex + ")";
-
-                    if (IE) {
-                        alphaSliderInner.css("filter", tinycolor(realAlpha).toFilter({ gradientType: 1 }, realHex));
-                    }
-                    else {
-                        alphaSliderInner.css("background", "-webkit-" + gradient);
-                        alphaSliderInner.css("background", "-moz-" + gradient);
-                        alphaSliderInner.css("background", "-ms-" + gradient);
-                        // Use current syntax gradient on unprefixed property.
-                        alphaSliderInner.css("background",
-                            "linear-gradient(to right, " + realAlpha + ", " + realHex + ")");
-                    }
-                }
-
-                displayColor = realColor.toString(format);
-            }
-
-            // Update the text entry input as it changes happen
-            if (opts.showInput) {
-                textInput.val(displayColor);
-            }
-
-            if (opts.showPalette) {
-                drawPalette();
-            }
-
-            drawInitial();
-        }
-
-        function updateHelperLocations() {
-            var s = currentSaturation;
-            var v = currentValue;
-
-            if(allowEmpty && isEmpty) {
-                //if selected color is empty, hide the helpers
-                alphaSlideHelper.hide();
-                slideHelper.hide();
-                dragHelper.hide();
-            }
-            else {
-                //make sure helpers are visible
-                alphaSlideHelper.show();
-                slideHelper.show();
-                dragHelper.show();
-
-                // Where to show the little circle in that displays your current selected color
-                var dragX = s * dragWidth;
-                var dragY = dragHeight - (v * dragHeight);
-                dragX = Math.max(
-                    -dragHelperHeight,
-                    Math.min(dragWidth - dragHelperHeight, dragX - dragHelperHeight)
-                );
-                dragY = Math.max(
-                    -dragHelperHeight,
-                    Math.min(dragHeight - dragHelperHeight, dragY - dragHelperHeight)
-                );
-                dragHelper.css({
-                    "top": dragY + "px",
-                    "left": dragX + "px"
-                });
-
-                var alphaX = currentAlpha * alphaWidth;
-                alphaSlideHelper.css({
-                    "left": (alphaX - (alphaSlideHelperWidth / 2)) + "px"
-                });
-
-                // Where to show the bar that displays your current selected hue
-                var slideY = (currentHue) * slideHeight;
-                slideHelper.css({
-                    "top": (slideY - slideHelperHeight) + "px"
-                });
-            }
-        }
-
-        function updateOriginalInput(fireCallback) {
-            var color = get(),
-                displayColor = '',
-                hasChanged = !tinycolor.equals(color, colorOnShow);
-
-            if (color) {
-                displayColor = color.toString(currentPreferredFormat);
-                // Update the selection palette with the current color
-                addColorToSelectionPalette(color);
-            }
-
-            if (isInput) {
-                boundElement.val(displayColor);
-            }
-
-            if (fireCallback && hasChanged) {
-                callbacks.change(color);
-                boundElement.trigger('change', [ color ]);
-            }
-        }
-
-        function reflow() {
-            dragWidth = dragger.width();
-            dragHeight = dragger.height();
-            dragHelperHeight = dragHelper.height();
-            slideWidth = slider.width();
-            slideHeight = slider.height();
-            slideHelperHeight = slideHelper.height();
-            alphaWidth = alphaSlider.width();
-            alphaSlideHelperWidth = alphaSlideHelper.width();
-
-            if (!flat) {
-                container.css("position", "absolute");
-                if (opts.offset) {
-                    container.offset(opts.offset);
-                } else {
-                    container.offset(getOffset(container, offsetElement));
-                }
-            }
-
-            updateHelperLocations();
-
-            if (opts.showPalette) {
-                drawPalette();
-            }
-
-            boundElement.trigger('reflow.spectrum');
-        }
-
-        function destroy() {
-            boundElement.show();
-            offsetElement.unbind("click.spectrum touchstart.spectrum");
-            container.remove();
-            replacer.remove();
-            spectrums[spect.id] = null;
-        }
-
-        function option(optionName, optionValue) {
-            if (optionName === undefined) {
-                return $.extend({}, opts);
-            }
-            if (optionValue === undefined) {
-                return opts[optionName];
-            }
-
-            opts[optionName] = optionValue;
-            applyOptions();
-        }
-
-        function enable() {
-            disabled = false;
-            boundElement.attr("disabled", false);
-            offsetElement.removeClass("sp-disabled");
-        }
-
-        function disable() {
-            hide();
-            disabled = true;
-            boundElement.attr("disabled", true);
-            offsetElement.addClass("sp-disabled");
-        }
-
-        function setOffset(coord) {
-            opts.offset = coord;
-            reflow();
-        }
-
-        initialize();
-
-        var spect = {
-            show: show,
-            hide: hide,
-            toggle: toggle,
-            reflow: reflow,
-            option: option,
-            enable: enable,
-            disable: disable,
-            offset: setOffset,
-            set: function (c) {
-                set(c);
-                updateOriginalInput();
-            },
-            get: get,
-            destroy: destroy,
-            container: container
-        };
-
-        spect.id = spectrums.push(spect) - 1;
-
-        return spect;
-    }
-
-    /**
-    * checkOffset - get the offset below/above and left/right element depending on screen position
-    * Thanks https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.datepicker.js
-    */
-    function getOffset(picker, input) {
-        var extraY = 0;
-        var dpWidth = picker.outerWidth();
-        var dpHeight = picker.outerHeight();
-        var inputHeight = input.outerHeight();
-        var doc = picker[0].ownerDocument;
-        var docElem = doc.documentElement;
-        var viewWidth = docElem.clientWidth + $(doc).scrollLeft();
-        var viewHeight = docElem.clientHeight + $(doc).scrollTop();
-        var offset = input.offset();
-        offset.top += inputHeight;
-
-        offset.left -=
-            Math.min(offset.left, (offset.left + dpWidth > viewWidth && viewWidth > dpWidth) ?
-            Math.abs(offset.left + dpWidth - viewWidth) : 0);
-
-        offset.top -=
-            Math.min(offset.top, ((offset.top + dpHeight > viewHeight && viewHeight > dpHeight) ?
-            Math.abs(dpHeight + inputHeight - extraY) : extraY));
-
-        return offset;
-    }
-
-    /**
-    * noop - do nothing
-    */
-    function noop() {
-
-    }
-
-    /**
-    * stopPropagation - makes the code only doing this a little easier to read in line
-    */
-    function stopPropagation(e) {
-        e.stopPropagation();
-    }
-
-    /**
-    * Create a function bound to a given object
-    * Thanks to underscore.js
-    */
-    function bind(func, obj) {
-        var slice = Array.prototype.slice;
-        var args = slice.call(arguments, 2);
-        return function () {
-            return func.apply(obj, args.concat(slice.call(arguments)));
-        };
-    }
-
-    /**
-    * Lightweight drag helper.  Handles containment within the element, so that
-    * when dragging, the x is within [0,element.width] and y is within [0,element.height]
-    */
-    function draggable(element, onmove, onstart, onstop) {
-        onmove = onmove || function () { };
-        onstart = onstart || function () { };
-        onstop = onstop || function () { };
-        var doc = document;
-        var dragging = false;
-        var offset = {};
-        var maxHeight = 0;
-        var maxWidth = 0;
-        var hasTouch = ('ontouchstart' in window);
-
-        var duringDragEvents = {};
-        duringDragEvents["selectstart"] = prevent;
-        duringDragEvents["dragstart"] = prevent;
-        duringDragEvents["touchmove mousemove"] = move;
-        duringDragEvents["touchend mouseup"] = stop;
-
-        function prevent(e) {
-            if (e.stopPropagation) {
-                e.stopPropagation();
-            }
-            if (e.preventDefault) {
-                e.preventDefault();
-            }
-            e.returnValue = false;
-        }
-
-        function move(e) {
-            if (dragging) {
-                // Mouseup happened outside of window
-                if (IE && doc.documentMode < 9 && !e.button) {
-                    return stop();
-                }
-
-                var t0 = e.originalEvent && e.originalEvent.touches && e.originalEvent.touches[0];
-                var pageX = t0 && t0.pageX || e.pageX;
-                var pageY = t0 && t0.pageY || e.pageY;
-
-                var dragX = Math.max(0, Math.min(pageX - offset.left, maxWidth));
-                var dragY = Math.max(0, Math.min(pageY - offset.top, maxHeight));
-
-                if (hasTouch) {
-                    // Stop scrolling in iOS
-                    prevent(e);
-                }
-
-                onmove.apply(element, [dragX, dragY, e]);
-            }
-        }
-
-        function start(e) {
-            var rightclick = (e.which) ? (e.which == 3) : (e.button == 2);
-
-            if (!rightclick && !dragging) {
-                if (onstart.apply(element, arguments) !== false) {
-                    dragging = true;
-                    maxHeight = $(element).height();
-                    maxWidth = $(element).width();
-                    offset = $(element).offset();
-
-                    $(doc).bind(duringDragEvents);
-                    $(doc.body).addClass("sp-dragging");
-
-                    move(e);
-
-                    prevent(e);
-                }
-            }
-        }
-
-        function stop() {
-            if (dragging) {
-                $(doc).unbind(duringDragEvents);
-                $(doc.body).removeClass("sp-dragging");
-
-                // Wait a tick before notifying observers to allow the click event
-                // to fire in Chrome.
-                setTimeout(function() {
-                    onstop.apply(element, arguments);
-                }, 0);
-            }
-            dragging = false;
-        }
-
-        $(element).bind("touchstart mousedown", start);
-    }
-
-    function throttle(func, wait, debounce) {
-        var timeout;
-        return function () {
-            var context = this, args = arguments;
-            var throttler = function () {
-                timeout = null;
-                func.apply(context, args);
-            };
-            if (debounce) clearTimeout(timeout);
-            if (debounce || !timeout) timeout = setTimeout(throttler, wait);
-        };
-    }
-
-    function inputTypeColorSupport() {
-        return $.fn.spectrum.inputTypeColorSupport();
-    }
-
-    /**
-    * Define a jQuery plugin
-    */
-    var dataID = "spectrum.id";
-    $.fn.spectrum = function (opts, extra) {
-
-        if (typeof opts == "string") {
-
-            var returnValue = this;
-            var args = Array.prototype.slice.call( arguments, 1 );
-
-            this.each(function () {
-                var spect = spectrums[$(this).data(dataID)];
-                if (spect) {
-                    var method = spect[opts];
-                    if (!method) {
-                        throw new Error( "Spectrum: no such method: '" + opts + "'" );
-                    }
-
-                    if (opts == "get") {
-                        returnValue = spect.get();
-                    }
-                    else if (opts == "container") {
-                        returnValue = spect.container;
-                    }
-                    else if (opts == "option") {
-                        returnValue = spect.option.apply(spect, args);
-                    }
-                    else if (opts == "destroy") {
-                        spect.destroy();
-                        $(this).removeData(dataID);
-                    }
-                    else {
-                        method.apply(spect, args);
-                    }
-                }
-            });
-
-            return returnValue;
-        }
-
-        // Initializing a new instance of spectrum
-        return this.spectrum("destroy").each(function () {
-            var options = $.extend({}, opts, $(this).data());
-            var spect = spectrum(this, options);
-            $(this).data(dataID, spect.id);
-        });
-    };
-
-    $.fn.spectrum.load = true;
-    $.fn.spectrum.loadOpts = {};
-    $.fn.spectrum.draggable = draggable;
-    $.fn.spectrum.defaults = defaultOpts;
-    $.fn.spectrum.inputTypeColorSupport = function inputTypeColorSupport() {
-        if (typeof inputTypeColorSupport._cachedResult === "undefined") {
-            var colorInput = $("<input type='color' value='!' />")[0];
-            inputTypeColorSupport._cachedResult = colorInput.type === "color" && colorInput.value !== "!";
-        }
-        return inputTypeColorSupport._cachedResult;
-    };
-
-    $.spectrum = { };
-    $.spectrum.localization = { };
-    $.spectrum.palettes = { };
-
-    $.fn.spectrum.processNativeColorInputs = function () {
-        var colorInputs = $("input[type=color]");
-        if (colorInputs.length && !inputTypeColorSupport()) {
-            colorInputs.spectrum({
-                preferredFormat: "hex6"
-            });
-        }
-    };
-
-    // TinyColor v1.1.2
-    // https://github.com/bgrins/TinyColor
-    // Brian Grinstead, MIT License
-
-    (function() {
-
-    var trimLeft = /^[\s,#]+/,
-        trimRight = /\s+$/,
-        tinyCounter = 0,
-        math = Math,
-        mathRound = math.round,
-        mathMin = math.min,
-        mathMax = math.max,
-        mathRandom = math.random;
-
-    var tinycolor = function(color, opts) {
-
-        color = (color) ? color : '';
-        opts = opts || { };
-
-        // If input is already a tinycolor, return itself
-        if (color instanceof tinycolor) {
-           return color;
-        }
-        // If we are called as a function, call using new instead
-        if (!(this instanceof tinycolor)) {
-            return new tinycolor(color, opts);
-        }
-
-        var rgb = inputToRGB(color);
-        this._originalInput = color,
-        this._r = rgb.r,
-        this._g = rgb.g,
-        this._b = rgb.b,
-        this._a = rgb.a,
-        this._roundA = mathRound(100*this._a) / 100,
-        this._format = opts.format || rgb.format;
-        this._gradientType = opts.gradientType;
-
-        // Don't let the range of [0,255] come back in [0,1].
-        // Potentially lose a little bit of precision here, but will fix issues where
-        // .5 gets interpreted as half of the total, instead of half of 1
-        // If it was supposed to be 128, this was already taken care of by `inputToRgb`
-        if (this._r < 1) { this._r = mathRound(this._r); }
-        if (this._g < 1) { this._g = mathRound(this._g); }
-        if (this._b < 1) { this._b = mathRound(this._b); }
-
-        this._ok = rgb.ok;
-        this._tc_id = tinyCounter++;
-    };
-
-    tinycolor.prototype = {
-        isDark: function() {
-            return this.getBrightness() < 128;
-        },
-        isLight: function() {
-            return !this.isDark();
-        },
-        isValid: function() {
-            return this._ok;
-        },
-        getOriginalInput: function() {
-          return this._originalInput;
-        },
-        getFormat: function() {
-            return this._format;
-        },
-        getAlpha: function() {
-            return this._a;
-        },
-        getBrightness: function() {
-            var rgb = this.toRgb();
-            return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
-        },
-        setAlpha: function(value) {
-            this._a = boundAlpha(value);
-            this._roundA = mathRound(100*this._a) / 100;
-            return this;
-        },
-        toHsv: function() {
-            var hsv = rgbToHsv(this._r, this._g, this._b);
-            return { h: hsv.h * 360, s: hsv.s, v: hsv.v, a: this._a };
-        },
-        toHsvString: function() {
-            var hsv = rgbToHsv(this._r, this._g, this._b);
-            var h = mathRound(hsv.h * 360), s = mathRound(hsv.s * 100), v = mathRound(hsv.v * 100);
-            return (this._a == 1) ?
-              "hsv("  + h + ", " + s + "%, " + v + "%)" :
-              "hsva(" + h + ", " + s + "%, " + v + "%, "+ this._roundA + ")";
-        },
-        toHsl: function() {
-            var hsl = rgbToHsl(this._r, this._g, this._b);
-            return { h: hsl.h * 360, s: hsl.s, l: hsl.l, a: this._a };
-        },
-        toHslString: function() {
-            var hsl = rgbToHsl(this._r, this._g, this._b);
-            var h = mathRound(hsl.h * 360), s = mathRound(hsl.s * 100), l = mathRound(hsl.l * 100);
-            return (this._a == 1) ?
-              "hsl("  + h + ", " + s + "%, " + l + "%)" :
-              "hsla(" + h + ", " + s + "%, " + l + "%, "+ this._roundA + ")";
-        },
-        toHex: function(allow3Char) {
-            return rgbToHex(this._r, this._g, this._b, allow3Char);
-        },
-        toHexString: function(allow3Char) {
-            return '#' + this.toHex(allow3Char);
-        },
-        toHex8: function() {
-            return rgbaToHex(this._r, this._g, this._b, this._a);
-        },
-        toHex8String: function() {
-            return '#' + this.toHex8();
-        },
-        toRgb: function() {
-            return { r: mathRound(this._r), g: mathRound(this._g), b: mathRound(this._b), a: this._a };
-        },
-        toRgbString: function() {
-            return (this._a == 1) ?
-              "rgb("  + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ")" :
-              "rgba(" + mathRound(this._r) + ", " + mathRound(this._g) + ", " + mathRound(this._b) + ", " + this._roundA + ")";
-        },
-        toPercentageRgb: function() {
-            return { r: mathRound(bound01(this._r, 255) * 100) + "%", g: mathRound(bound01(this._g, 255) * 100) + "%", b: mathRound(bound01(this._b, 255) * 100) + "%", a: this._a };
-        },
-        toPercentageRgbString: function() {
-            return (this._a == 1) ?
-              "rgb("  + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%)" :
-              "rgba(" + mathRound(bound01(this._r, 255) * 100) + "%, " + mathRound(bound01(this._g, 255) * 100) + "%, " + mathRound(bound01(this._b, 255) * 100) + "%, " + this._roundA + ")";
-        },
-        toName: function() {
-            if (this._a === 0) {
-                return "transparent";
-            }
-
-            if (this._a < 1) {
-                return false;
-            }
-
-            return hexNames[rgbToHex(this._r, this._g, this._b, true)] || false;
-        },
-        toFilter: function(secondColor) {
-            var hex8String = '#' + rgbaToHex(this._r, this._g, this._b, this._a);
-            var secondHex8String = hex8String;
-            var gradientType = this._gradientType ? "GradientType = 1, " : "";
-
-            if (secondColor) {
-                var s = tinycolor(secondColor);
-                secondHex8String = s.toHex8String();
-            }
-
-            return "progid:DXImageTransform.Microsoft.gradient("+gradientType+"startColorstr="+hex8String+",endColorstr="+secondHex8String+")";
-        },
-        toString: function(format) {
-            var formatSet = !!format;
-            format = format || this._format;
-
-            var formattedString = false;
-            var hasAlpha = this._a < 1 && this._a >= 0;
-            var needsAlphaFormat = !formatSet && hasAlpha && (format === "hex" || format === "hex6" || format === "hex3" || format === "name");
-
-            if (needsAlphaFormat) {
-                // Special case for "transparent", all other non-alpha formats
-                // will return rgba when there is transparency.
-                if (format === "name" && this._a === 0) {
-                    return this.toName();
-                }
-                return this.toRgbString();
-            }
-            if (format === "rgb") {
-                formattedString = this.toRgbString();
-            }
-            if (format === "prgb") {
-                formattedString = this.toPercentageRgbString();
-            }
-            if (format === "hex" || format === "hex6") {
-                formattedString = this.toHexString();
-            }
-            if (format === "hex3") {
-                formattedString = this.toHexString(true);
-            }
-            if (format === "hex8") {
-                formattedString = this.toHex8String();
-            }
-            if (format === "name") {
-                formattedString = this.toName();
-            }
-            if (format === "hsl") {
-                formattedString = this.toHslString();
-            }
-            if (format === "hsv") {
-                formattedString = this.toHsvString();
-            }
-
-            return formattedString || this.toHexString();
-        },
-
-        _applyModification: function(fn, args) {
-            var color = fn.apply(null, [this].concat([].slice.call(args)));
-            this._r = color._r;
-            this._g = color._g;
-            this._b = color._b;
-            this.setAlpha(color._a);
-            return this;
-        },
-        lighten: function() {
-            return this._applyModification(lighten, arguments);
-        },
-        brighten: function() {
-            return this._applyModification(brighten, arguments);
-        },
-        darken: function() {
-            return this._applyModification(darken, arguments);
-        },
-        desaturate: function() {
-            return this._applyModification(desaturate, arguments);
-        },
-        saturate: function() {
-            return this._applyModification(saturate, arguments);
-        },
-        greyscale: function() {
-            return this._applyModification(greyscale, arguments);
-        },
-        spin: function() {
-            return this._applyModification(spin, arguments);
-        },
-
-        _applyCombination: function(fn, args) {
-            return fn.apply(null, [this].concat([].slice.call(args)));
-        },
-        analogous: function() {
-            return this._applyCombination(analogous, arguments);
-        },
-        complement: function() {
-            return this._applyCombination(complement, arguments);
-        },
-        monochromatic: function() {
-            return this._applyCombination(monochromatic, arguments);
-        },
-        splitcomplement: function() {
-            return this._applyCombination(splitcomplement, arguments);
-        },
-        triad: function() {
-            return this._applyCombination(triad, arguments);
-        },
-        tetrad: function() {
-            return this._applyCombination(tetrad, arguments);
-        }
-    };
-
-    // If input is an object, force 1 into "1.0" to handle ratios properly
-    // String input requires "1.0" as input, so 1 will be treated as 1
-    tinycolor.fromRatio = function(color, opts) {
-        if (typeof color == "object") {
-            var newColor = {};
-            for (var i in color) {
-                if (color.hasOwnProperty(i)) {
-                    if (i === "a") {
-                        newColor[i] = color[i];
-                    }
-                    else {
-                        newColor[i] = convertToPercentage(color[i]);
-                    }
-                }
-            }
-            color = newColor;
-        }
-
-        return tinycolor(color, opts);
-    };
-
-    // Given a string or object, convert that input to RGB
-    // Possible string inputs:
-    //
-    //     "red"
-    //     "#f00" or "f00"
-    //     "#ff0000" or "ff0000"
-    //     "#ff000000" or "ff000000"
-    //     "rgb 255 0 0" or "rgb (255, 0, 0)"
-    //     "rgb 1.0 0 0" or "rgb (1, 0, 0)"
-    //     "rgba (255, 0, 0, 1)" or "rgba 255, 0, 0, 1"
-    //     "rgba (1.0, 0, 0, 1)" or "rgba 1.0, 0, 0, 1"
-    //     "hsl(0, 100%, 50%)" or "hsl 0 100% 50%"
-    //     "hsla(0, 100%, 50%, 1)" or "hsla 0 100% 50%, 1"
-    //     "hsv(0, 100%, 100%)" or "hsv 0 100% 100%"
-    //
-    function inputToRGB(color) {
-
-        var rgb = { r: 0, g: 0, b: 0 };
-        var a = 1;
-        var ok = false;
-        var format = false;
-
-        if (typeof color == "string") {
-            color = stringInputToObject(color);
-        }
-
-        if (typeof color == "object") {
-            if (color.hasOwnProperty("r") && color.hasOwnProperty("g") && color.hasOwnProperty("b")) {
-                rgb = rgbToRgb(color.r, color.g, color.b);
-                ok = true;
-                format = String(color.r).substr(-1) === "%" ? "prgb" : "rgb";
-            }
-            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("v")) {
-                color.s = convertToPercentage(color.s);
-                color.v = convertToPercentage(color.v);
-                rgb = hsvToRgb(color.h, color.s, color.v);
-                ok = true;
-                format = "hsv";
-            }
-            else if (color.hasOwnProperty("h") && color.hasOwnProperty("s") && color.hasOwnProperty("l")) {
-                color.s = convertToPercentage(color.s);
-                color.l = convertToPercentage(color.l);
-                rgb = hslToRgb(color.h, color.s, color.l);
-                ok = true;
-                format = "hsl";
-            }
-
-            if (color.hasOwnProperty("a")) {
-                a = color.a;
-            }
-        }
-
-        a = boundAlpha(a);
-
-        return {
-            ok: ok,
-            format: color.format || format,
-            r: mathMin(255, mathMax(rgb.r, 0)),
-            g: mathMin(255, mathMax(rgb.g, 0)),
-            b: mathMin(255, mathMax(rgb.b, 0)),
-            a: a
-        };
-    }
-
-
-    // Conversion Functions
-    // --------------------
-
-    // `rgbToHsl`, `rgbToHsv`, `hslToRgb`, `hsvToRgb` modified from:
-    // <http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript>
-
-    // `rgbToRgb`
-    // Handle bounds / percentage checking to conform to CSS color spec
-    // <http://www.w3.org/TR/css3-color/>
-    // *Assumes:* r, g, b in [0, 255] or [0, 1]
-    // *Returns:* { r, g, b } in [0, 255]
-    function rgbToRgb(r, g, b){
-        return {
-            r: bound01(r, 255) * 255,
-            g: bound01(g, 255) * 255,
-            b: bound01(b, 255) * 255
-        };
-    }
-
-    // `rgbToHsl`
-    // Converts an RGB color value to HSL.
-    // *Assumes:* r, g, and b are contained in [0, 255] or [0, 1]
-    // *Returns:* { h, s, l } in [0,1]
-    function rgbToHsl(r, g, b) {
-
-        r = bound01(r, 255);
-        g = bound01(g, 255);
-        b = bound01(b, 255);
-
-        var max = mathMax(r, g, b), min = mathMin(r, g, b);
-        var h, s, l = (max + min) / 2;
-
-        if(max == min) {
-            h = s = 0; // achromatic
-        }
-        else {
-            var d = max - min;
-            s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
-            switch(max) {
-                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
-                case g: h = (b - r) / d + 2; break;
-                case b: h = (r - g) / d + 4; break;
-            }
-
-            h /= 6;
-        }
-
-        return { h: h, s: s, l: l };
-    }
-
-    // `hslToRgb`
-    // Converts an HSL color value to RGB.
-    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and l are contained [0, 1] or [0, 100]
-    // *Returns:* { r, g, b } in the set [0, 255]
-    function hslToRgb(h, s, l) {
-        var r, g, b;
-
-        h = bound01(h, 360);
-        s = bound01(s, 100);
-        l = bound01(l, 100);
-
-        function hue2rgb(p, q, t) {
-            if(t < 0) t += 1;
-            if(t > 1) t -= 1;
-            if(t < 1/6) return p + (q - p) * 6 * t;
-            if(t < 1/2) return q;
-            if(t < 2/3) return p + (q - p) * (2/3 - t) * 6;
-            return p;
-        }
-
-        if(s === 0) {
-            r = g = b = l; // achromatic
-        }
-        else {
-            var q = l < 0.5 ? l * (1 + s) : l + s - l * s;
-            var p = 2 * l - q;
-            r = hue2rgb(p, q, h + 1/3);
-            g = hue2rgb(p, q, h);
-            b = hue2rgb(p, q, h - 1/3);
-        }
-
-        return { r: r * 255, g: g * 255, b: b * 255 };
-    }
-
-    // `rgbToHsv`
-    // Converts an RGB color value to HSV
-    // *Assumes:* r, g, and b are contained in the set [0, 255] or [0, 1]
-    // *Returns:* { h, s, v } in [0,1]
-    function rgbToHsv(r, g, b) {
-
-        r = bound01(r, 255);
-        g = bound01(g, 255);
-        b = bound01(b, 255);
-
-        var max = mathMax(r, g, b), min = mathMin(r, g, b);
-        var h, s, v = max;
-
-        var d = max - min;
-        s = max === 0 ? 0 : d / max;
-
-        if(max == min) {
-            h = 0; // achromatic
-        }
-        else {
-            switch(max) {
-                case r: h = (g - b) / d + (g < b ? 6 : 0); break;
-                case g: h = (b - r) / d + 2; break;
-                case b: h = (r - g) / d + 4; break;
-            }
-            h /= 6;
-        }
-        return { h: h, s: s, v: v };
-    }
-
-    // `hsvToRgb`
-    // Converts an HSV color value to RGB.
-    // *Assumes:* h is contained in [0, 1] or [0, 360] and s and v are contained in [0, 1] or [0, 100]
-    // *Returns:* { r, g, b } in the set [0, 255]
-     function hsvToRgb(h, s, v) {
-
-        h = bound01(h, 360) * 6;
-        s = bound01(s, 100);
-        v = bound01(v, 100);
-
-        var i = math.floor(h),
-            f = h - i,
-            p = v * (1 - s),
-            q = v * (1 - f * s),
-            t = v * (1 - (1 - f) * s),
-            mod = i % 6,
-            r = [v, q, p, p, t, v][mod],
-            g = [t, v, v, q, p, p][mod],
-            b = [p, p, t, v, v, q][mod];
-
-        return { r: r * 255, g: g * 255, b: b * 255 };
-    }
-
-    // `rgbToHex`
-    // Converts an RGB color to hex
-    // Assumes r, g, and b are contained in the set [0, 255]
-    // Returns a 3 or 6 character hex
-    function rgbToHex(r, g, b, allow3Char) {
-
-        var hex = [
-            pad2(mathRound(r).toString(16)),
-            pad2(mathRound(g).toString(16)),
-            pad2(mathRound(b).toString(16))
-        ];
-
-        // Return a 3 character hex if possible
-        if (allow3Char && hex[0].charAt(0) == hex[0].charAt(1) && hex[1].charAt(0) == hex[1].charAt(1) && hex[2].charAt(0) == hex[2].charAt(1)) {
-            return hex[0].charAt(0) + hex[1].charAt(0) + hex[2].charAt(0);
-        }
-
-        return hex.join("");
-    }
-        // `rgbaToHex`
-        // Converts an RGBA color plus alpha transparency to hex
-        // Assumes r, g, b and a are contained in the set [0, 255]
-        // Returns an 8 character hex
-        function rgbaToHex(r, g, b, a) {
-
-            var hex = [
-                pad2(convertDecimalToHex(a)),
-                pad2(mathRound(r).toString(16)),
-                pad2(mathRound(g).toString(16)),
-                pad2(mathRound(b).toString(16))
-            ];
-
-            return hex.join("");
-        }
-
-    // `equals`
-    // Can be called with any tinycolor input
-    tinycolor.equals = function (color1, color2) {
-        if (!color1 || !color2) { return false; }
-        return tinycolor(color1).toRgbString() == tinycolor(color2).toRgbString();
-    };
-    tinycolor.random = function() {
-        return tinycolor.fromRatio({
-            r: mathRandom(),
-            g: mathRandom(),
-            b: mathRandom()
-        });
-    };
-
-
-    // Modification Functions
-    // ----------------------
-    // Thanks to less.js for some of the basics here
-    // <https://github.com/cloudhead/less.js/blob/master/lib/less/functions.js>
-
-    function desaturate(color, amount) {
-        amount = (amount === 0) ? 0 : (amount || 10);
-        var hsl = tinycolor(color).toHsl();
-        hsl.s -= amount / 100;
-        hsl.s = clamp01(hsl.s);
-        return tinycolor(hsl);
-    }
-
-    function saturate(color, amount) {
-        amount = (amount === 0) ? 0 : (amount || 10);
-        var hsl = tinycolor(color).toHsl();
-        hsl.s += amount / 100;
-        hsl.s = clamp01(hsl.s);
-        return tinycolor(hsl);
-    }
-
-    function greyscale(color) {
-        return tinycolor(color).desaturate(100);
-    }
-
-    function lighten (color, amount) {
-        amount = (amount === 0) ? 0 : (amount || 10);
-        var hsl = tinycolor(color).toHsl();
-        hsl.l += amount / 100;
-        hsl.l = clamp01(hsl.l);
-        return tinycolor(hsl);
-    }
-
-    function brighten(color, amount) {
-        amount = (amount === 0) ? 0 : (amount || 10);
-        var rgb = tinycolor(color).toRgb();
-        rgb.r = mathMax(0, mathMin(255, rgb.r - mathRound(255 * - (amount / 100))));
-        rgb.g = mathMax(0, mathMin(255, rgb.g - mathRound(255 * - (amount / 100))));
-        rgb.b = mathMax(0, mathMin(255, rgb.b - mathRound(255 * - (amount / 100))));
-        return tinycolor(rgb);
-    }
-
-    function darken (color, amount) {
-        amount = (amount === 0) ? 0 : (amount || 10);
-        var hsl = tinycolor(color).toHsl();
-        hsl.l -= amount / 100;
-        hsl.l = clamp01(hsl.l);
-        return tinycolor(hsl);
-    }
-
-    // Spin takes a positive or negative amount within [-360, 360] indicating the change of hue.
-    // Values outside of this range will be wrapped into this range.
-    function spin(color, amount) {
-        var hsl = tinycolor(color).toHsl();
-        var hue = (mathRound(hsl.h) + amount) % 360;
-        hsl.h = hue < 0 ? 360 + hue : hue;
-        return tinycolor(hsl);
-    }
-
-    // Combination Functions
-    // ---------------------
-    // Thanks to jQuery xColor for some of the ideas behind these
-    // <https://github.com/infusion/jQuery-xcolor/blob/master/jquery.xcolor.js>
-
-    function complement(color) {
-        var hsl = tinycolor(color).toHsl();
-        hsl.h = (hsl.h + 180) % 360;
-        return tinycolor(hsl);
-    }
-
-    function triad(color) {
-        var hsl = tinycolor(color).toHsl();
-        var h = hsl.h;
-        return [
-            tinycolor(color),
-            tinycolor({ h: (h + 120) % 360, s: hsl.s, l: hsl.l }),
-            tinycolor({ h: (h + 240) % 360, s: hsl.s, l: hsl.l })
-        ];
-    }
-
-    function tetrad(color) {
-        var hsl = tinycolor(color).toHsl();
-        var h = hsl.h;
-        return [
-            tinycolor(color),
-            tinycolor({ h: (h + 90) % 360, s: hsl.s, l: hsl.l }),
-            tinycolor({ h: (h + 180) % 360, s: hsl.s, l: hsl.l }),
-            tinycolor({ h: (h + 270) % 360, s: hsl.s, l: hsl.l })
-        ];
-    }
-
-    function splitcomplement(color) {
-        var hsl = tinycolor(color).toHsl();
-        var h = hsl.h;
-        return [
-            tinycolor(color),
-            tinycolor({ h: (h + 72) % 360, s: hsl.s, l: hsl.l}),
-            tinycolor({ h: (h + 216) % 360, s: hsl.s, l: hsl.l})
-        ];
-    }
-
-    function analogous(color, results, slices) {
-        results = results || 6;
-        slices = slices || 30;
-
-        var hsl = tinycolor(color).toHsl();
-        var part = 360 / slices;
-        var ret = [tinycolor(color)];
-
-        for (hsl.h = ((hsl.h - (part * results >> 1)) + 720) % 360; --results; ) {
-            hsl.h = (hsl.h + part) % 360;
-            ret.push(tinycolor(hsl));
-        }
-        return ret;
-    }
-
-    function monochromatic(color, results) {
-        results = results || 6;
-        var hsv = tinycolor(color).toHsv();
-        var h = hsv.h, s = hsv.s, v = hsv.v;
-        var ret = [];
-        var modification = 1 / results;
-
-        while (results--) {
-            ret.push(tinycolor({ h: h, s: s, v: v}));
-            v = (v + modification) % 1;
-        }
-
-        return ret;
-    }
-
-    // Utility Functions
-    // ---------------------
-
-    tinycolor.mix = function(color1, color2, amount) {
-        amount = (amount === 0) ? 0 : (amount || 50);
-
-        var rgb1 = tinycolor(color1).toRgb();
-        var rgb2 = tinycolor(color2).toRgb();
-
-        var p = amount / 100;
-        var w = p * 2 - 1;
-        var a = rgb2.a - rgb1.a;
-
-        var w1;
-
-        if (w * a == -1) {
-            w1 = w;
-        } else {
-            w1 = (w + a) / (1 + w * a);
-        }
-
-        w1 = (w1 + 1) / 2;
-
-        var w2 = 1 - w1;
-
-        var rgba = {
-            r: rgb2.r * w1 + rgb1.r * w2,
-            g: rgb2.g * w1 + rgb1.g * w2,
-            b: rgb2.b * w1 + rgb1.b * w2,
-            a: rgb2.a * p  + rgb1.a * (1 - p)
-        };
-
-        return tinycolor(rgba);
-    };
-
-
-    // Readability Functions
-    // ---------------------
-    // <http://www.w3.org/TR/AERT#color-contrast>
-
-    // `readability`
-    // Analyze the 2 colors and returns an object with the following properties:
-    //    `brightness`: difference in brightness between the two colors
-    //    `color`: difference in color/hue between the two colors
-    tinycolor.readability = function(color1, color2) {
-        var c1 = tinycolor(color1);
-        var c2 = tinycolor(color2);
-        var rgb1 = c1.toRgb();
-        var rgb2 = c2.toRgb();
-        var brightnessA = c1.getBrightness();
-        var brightnessB = c2.getBrightness();
-        var colorDiff = (
-            Math.max(rgb1.r, rgb2.r) - Math.min(rgb1.r, rgb2.r) +
-            Math.max(rgb1.g, rgb2.g) - Math.min(rgb1.g, rgb2.g) +
-            Math.max(rgb1.b, rgb2.b) - Math.min(rgb1.b, rgb2.b)
-        );
-
-        return {
-            brightness: Math.abs(brightnessA - brightnessB),
-            color: colorDiff
-        };
-    };
-
-    // `readable`
-    // http://www.w3.org/TR/AERT#color-contrast
-    // Ensure that foreground and background color combinations provide sufficient contrast.
-    // *Example*
-    //    tinycolor.isReadable("#000", "#111") => false
-    tinycolor.isReadable = function(color1, color2) {
-        var readability = tinycolor.readability(color1, color2);
-        return readability.brightness > 125 && readability.color > 500;
-    };
-
-    // `mostReadable`
-    // Given a base color and a list of possible foreground or background
-    // colors for that base, returns the most readable color.
-    // *Example*
-    //    tinycolor.mostReadable("#123", ["#fff", "#000"]) => "#000"
-    tinycolor.mostReadable = function(baseColor, colorList) {
-        var bestColor = null;
-        var bestScore = 0;
-        var bestIsReadable = false;
-        for (var i=0; i < colorList.length; i++) {
-
-            // We normalize both around the "acceptable" breaking point,
-            // but rank brightness constrast higher than hue.
-
-            var readability = tinycolor.readability(baseColor, colorList[i]);
-            var readable = readability.brightness > 125 && readability.color > 500;
-            var score = 3 * (readability.brightness / 125) + (readability.color / 500);
-
-            if ((readable && ! bestIsReadable) ||
-                (readable && bestIsReadable && score > bestScore) ||
-                ((! readable) && (! bestIsReadable) && score > bestScore)) {
-                bestIsReadable = readable;
-                bestScore = score;
-                bestColor = tinycolor(colorList[i]);
-            }
-        }
-        return bestColor;
-    };
-
-
-    // Big List of Colors
-    // ------------------
-    // <http://www.w3.org/TR/css3-color/#svg-color>
-    var names = tinycolor.names = {
-        aliceblue: "f0f8ff",
-        antiquewhite: "faebd7",
-        aqua: "0ff",
-        aquamarine: "7fffd4",
-        azure: "f0ffff",
-        beige: "f5f5dc",
-        bisque: "ffe4c4",
-        black: "000",
-        blanchedalmond: "ffebcd",
-        blue: "00f",
-        blueviolet: "8a2be2",
-        brown: "a52a2a",
-        burlywood: "deb887",
-        burntsienna: "ea7e5d",
-        cadetblue: "5f9ea0",
-        chartreuse: "7fff00",
-        chocolate: "d2691e",
-        coral: "ff7f50",
-        cornflowerblue: "6495ed",
-        cornsilk: "fff8dc",
-        crimson: "dc143c",
-        cyan: "0ff",
-        darkblue: "00008b",
-        darkcyan: "008b8b",
-        darkgoldenrod: "b8860b",
-        darkgray: "a9a9a9",
-        darkgreen: "006400",
-        darkgrey: "a9a9a9",
-        darkkhaki: "bdb76b",
-        darkmagenta: "8b008b",
-        darkolivegreen: "556b2f",
-        darkorange: "ff8c00",
-        darkorchid: "9932cc",
-        darkred: "8b0000",
-        darksalmon: "e9967a",
-        darkseagreen: "8fbc8f",
-        darkslateblue: "483d8b",
-        darkslategray: "2f4f4f",
-        darkslategrey: "2f4f4f",
-        darkturquoise: "00ced1",
-        darkviolet: "9400d3",
-        deeppink: "ff1493",
-        deepskyblue: "00bfff",
-        dimgray: "696969",
-        dimgrey: "696969",
-        dodgerblue: "1e90ff",
-        firebrick: "b22222",
-        floralwhite: "fffaf0",
-        forestgreen: "228b22",
-        fuchsia: "f0f",
-        gainsboro: "dcdcdc",
-        ghostwhite: "f8f8ff",
-        gold: "ffd700",
-        goldenrod: "daa520",
-        gray: "808080",
-        green: "008000",
-        greenyellow: "adff2f",
-        grey: "808080",
-        honeydew: "f0fff0",
-        hotpink: "ff69b4",
-        indianred: "cd5c5c",
-        indigo: "4b0082",
-        ivory: "fffff0",
-        khaki: "f0e68c",
-        lavender: "e6e6fa",
-        lavenderblush: "fff0f5",
-        lawngreen: "7cfc00",
-        lemonchiffon: "fffacd",
-        lightblue: "add8e6",
-        lightcoral: "f08080",
-        lightcyan: "e0ffff",
-        lightgoldenrodyellow: "fafad2",
-        lightgray: "d3d3d3",
-        lightgreen: "90ee90",
-        lightgrey: "d3d3d3",
-        lightpink: "ffb6c1",
-        lightsalmon: "ffa07a",
-        lightseagreen: "20b2aa",
-        lightskyblue: "87cefa",
-        lightslategray: "789",
-        lightslategrey: "789",
-        lightsteelblue: "b0c4de",
-        lightyellow: "ffffe0",
-        lime: "0f0",
-        limegreen: "32cd32",
-        linen: "faf0e6",
-        magenta: "f0f",
-        maroon: "800000",
-        mediumaquamarine: "66cdaa",
-        mediumblue: "0000cd",
-        mediumorchid: "ba55d3",
-        mediumpurple: "9370db",
-        mediumseagreen: "3cb371",
-        mediumslateblue: "7b68ee",
-        mediumspringgreen: "00fa9a",
-        mediumturquoise: "48d1cc",
-        mediumvioletred: "c71585",
-        midnightblue: "191970",
-        mintcream: "f5fffa",
-        mistyrose: "ffe4e1",
-        moccasin: "ffe4b5",
-        navajowhite: "ffdead",
-        navy: "000080",
-        oldlace: "fdf5e6",
-        olive: "808000",
-        olivedrab: "6b8e23",
-        orange: "ffa500",
-        orangered: "ff4500",
-        orchid: "da70d6",
-        palegoldenrod: "eee8aa",
-        palegreen: "98fb98",
-        paleturquoise: "afeeee",
-        palevioletred: "db7093",
-        papayawhip: "ffefd5",
-        peachpuff: "ffdab9",
-        peru: "cd853f",
-        pink: "ffc0cb",
-        plum: "dda0dd",
-        powderblue: "b0e0e6",
-        purple: "800080",
-        rebeccapurple: "663399",
-        red: "f00",
-        rosybrown: "bc8f8f",
-        royalblue: "4169e1",
-        saddlebrown: "8b4513",
-        salmon: "fa8072",
-        sandybrown: "f4a460",
-        seagreen: "2e8b57",
-        seashell: "fff5ee",
-        sienna: "a0522d",
-        silver: "c0c0c0",
-        skyblue: "87ceeb",
-        slateblue: "6a5acd",
-        slategray: "708090",
-        slategrey: "708090",
-        snow: "fffafa",
-        springgreen: "00ff7f",
-        steelblue: "4682b4",
-        tan: "d2b48c",
-        teal: "008080",
-        thistle: "d8bfd8",
-        tomato: "ff6347",
-        turquoise: "40e0d0",
-        violet: "ee82ee",
-        wheat: "f5deb3",
-        white: "fff",
-        whitesmoke: "f5f5f5",
-        yellow: "ff0",
-        yellowgreen: "9acd32"
-    };
-
-    // Make it easy to access colors via `hexNames[hex]`
-    var hexNames = tinycolor.hexNames = flip(names);
-
-
-    // Utilities
-    // ---------
-
-    // `{ 'name1': 'val1' }` becomes `{ 'val1': 'name1' }`
-    function flip(o) {
-        var flipped = { };
-        for (var i in o) {
-            if (o.hasOwnProperty(i)) {
-                flipped[o[i]] = i;
-            }
-        }
-        return flipped;
-    }
-
-    // Return a valid alpha value [0,1] with all invalid values being set to 1
-    function boundAlpha(a) {
-        a = parseFloat(a);
-
-        if (isNaN(a) || a < 0 || a > 1) {
-            a = 1;
-        }
-
-        return a;
-    }
-
-    // Take input from [0, n] and return it as [0, 1]
-    function bound01(n, max) {
-        if (isOnePointZero(n)) { n = "100%"; }
-
-        var processPercent = isPercentage(n);
-        n = mathMin(max, mathMax(0, parseFloat(n)));
-
-        // Automatically convert percentage into number
-        if (processPercent) {
-            n = parseInt(n * max, 10) / 100;
-        }
-
-        // Handle floating point rounding errors
-        if ((math.abs(n - max) < 0.000001)) {
-            return 1;
-        }
-
-        // Convert into [0, 1] range if it isn't already
-        return (n % max) / parseFloat(max);
-    }
-
-    // Force a number between 0 and 1
-    function clamp01(val) {
-        return mathMin(1, mathMax(0, val));
-    }
-
-    // Parse a base-16 hex value into a base-10 integer
-    function parseIntFromHex(val) {
-        return parseInt(val, 16);
-    }
-
-    // Need to handle 1.0 as 100%, since once it is a number, there is no difference between it and 1
-    // <http://stackoverflow.com/questions/7422072/javascript-how-to-detect-number-as-a-decimal-including-1-0>
-    function isOnePointZero(n) {
-        return typeof n == "string" && n.indexOf('.') != -1 && parseFloat(n) === 1;
-    }
-
-    // Check to see if string passed in is a percentage
-    function isPercentage(n) {
-        return typeof n === "string" && n.indexOf('%') != -1;
-    }
-
-    // Force a hex value to have 2 characters
-    function pad2(c) {
-        return c.length == 1 ? '0' + c : '' + c;
-    }
-
-    // Replace a decimal with it's percentage value
-    function convertToPercentage(n) {
-        if (n <= 1) {
-            n = (n * 100) + "%";
-        }
-
-        return n;
-    }
-
-    // Converts a decimal to a hex value
-    function convertDecimalToHex(d) {
-        return Math.round(parseFloat(d) * 255).toString(16);
-    }
-    // Converts a hex value to a decimal
-    function convertHexToDecimal(h) {
-        return (parseIntFromHex(h) / 255);
-    }
-
-    var matchers = (function() {
-
-        // <http://www.w3.org/TR/css3-values/#integers>
-        var CSS_INTEGER = "[-\\+]?\\d+%?";
-
-        // <http://www.w3.org/TR/css3-values/#number-value>
-        var CSS_NUMBER = "[-\\+]?\\d*\\.\\d+%?";
-
-        // Allow positive/negative integer/number.  Don't capture the either/or, just the entire outcome.
-        var CSS_UNIT = "(?:" + CSS_NUMBER + ")|(?:" + CSS_INTEGER + ")";
-
-        // Actual matching.
-        // Parentheses and commas are optional, but not required.
-        // Whitespace can take the place of commas or opening paren
-        var PERMISSIVE_MATCH3 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
-        var PERMISSIVE_MATCH4 = "[\\s|\\(]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")[,|\\s]+(" + CSS_UNIT + ")\\s*\\)?";
-
-        return {
-            rgb: new RegExp("rgb" + PERMISSIVE_MATCH3),
-            rgba: new RegExp("rgba" + PERMISSIVE_MATCH4),
-            hsl: new RegExp("hsl" + PERMISSIVE_MATCH3),
-            hsla: new RegExp("hsla" + PERMISSIVE_MATCH4),
-            hsv: new RegExp("hsv" + PERMISSIVE_MATCH3),
-            hsva: new RegExp("hsva" + PERMISSIVE_MATCH4),
-            hex3: /^([0-9a-fA-F]{1})([0-9a-fA-F]{1})([0-9a-fA-F]{1})$/,
-            hex6: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
-            hex8: /^([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/
-        };
-    })();
-
-    // `stringInputToObject`
-    // Permissive string parsing.  Take in a number of formats, and output an object
-    // based on detected format.  Returns `{ r, g, b }` or `{ h, s, l }` or `{ h, s, v}`
-    function stringInputToObject(color) {
-
-        color = color.replace(trimLeft,'').replace(trimRight, '').toLowerCase();
-        var named = false;
-        if (names[color]) {
-            color = names[color];
-            named = true;
-        }
-        else if (color == 'transparent') {
-            return { r: 0, g: 0, b: 0, a: 0, format: "name" };
-        }
-
-        // Try to match string input using regular expressions.
-        // Keep most of the number bounding out of this function - don't worry about [0,1] or [0,100] or [0,360]
-        // Just return an object and let the conversion functions handle that.
-        // This way the result will be the same whether the tinycolor is initialized with string or object.
-        var match;
-        if ((match = matchers.rgb.exec(color))) {
-            return { r: match[1], g: match[2], b: match[3] };
-        }
-        if ((match = matchers.rgba.exec(color))) {
-            return { r: match[1], g: match[2], b: match[3], a: match[4] };
-        }
-        if ((match = matchers.hsl.exec(color))) {
-            return { h: match[1], s: match[2], l: match[3] };
-        }
-        if ((match = matchers.hsla.exec(color))) {
-            return { h: match[1], s: match[2], l: match[3], a: match[4] };
-        }
-        if ((match = matchers.hsv.exec(color))) {
-            return { h: match[1], s: match[2], v: match[3] };
-        }
-        if ((match = matchers.hsva.exec(color))) {
-            return { h: match[1], s: match[2], v: match[3], a: match[4] };
-        }
-        if ((match = matchers.hex8.exec(color))) {
-            return {
-                a: convertHexToDecimal(match[1]),
-                r: parseIntFromHex(match[2]),
-                g: parseIntFromHex(match[3]),
-                b: parseIntFromHex(match[4]),
-                format: named ? "name" : "hex8"
-            };
-        }
-        if ((match = matchers.hex6.exec(color))) {
-            return {
-                r: parseIntFromHex(match[1]),
-                g: parseIntFromHex(match[2]),
-                b: parseIntFromHex(match[3]),
-                format: named ? "name" : "hex"
-            };
-        }
-        if ((match = matchers.hex3.exec(color))) {
-            return {
-                r: parseIntFromHex(match[1] + '' + match[1]),
-                g: parseIntFromHex(match[2] + '' + match[2]),
-                b: parseIntFromHex(match[3] + '' + match[3]),
-                format: named ? "name" : "hex"
-            };
-        }
-
-        return false;
-    }
-
-    window.tinycolor = tinycolor;
-    })();
-
-    $(function () {
-        if ($.fn.spectrum.load) {
-            $.fn.spectrum.processNativeColorInputs();
-        }
-    });
-
-});

+ 7 - 4
packages/grafana-ui/tsconfig.json

@@ -5,14 +5,17 @@
     "src/**/*.tsx"
   ],
   "exclude": [
-    "dist"
+    "dist",
+    "node_modules"
   ],
   "compilerOptions": {
-    "rootDir": ".",
+    "rootDirs": [".", "stories"],
     "module": "esnext",
     "outDir": "dist",
     "declaration": true,
     "noImplicitAny": true,
-    "strictNullChecks": true
-  }
+    "strictNullChecks": true,
+    "typeRoots": ["./node_modules/@types", "types"],
+    "skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors
+  },
 }

+ 6 - 6
pkg/api/api.go

@@ -23,9 +23,9 @@ func (hs *HTTPServer) registerRoutes() {
 
 	// not logged in views
 	r.Get("/", reqSignedIn, hs.Index)
-	r.Get("/logout", Logout)
-	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost))
-	r.Get("/login/:name", quota("session"), OAuthLogin)
+	r.Get("/logout", hs.Logout)
+	r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(hs.LoginPost))
+	r.Get("/login/:name", quota("session"), hs.OAuthLogin)
 	r.Get("/login", hs.LoginView)
 	r.Get("/invite/:code", hs.Index)
 
@@ -84,11 +84,11 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Get("/signup", hs.Index)
 	r.Get("/api/user/signup/options", Wrap(GetSignUpOptions))
 	r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp))
-	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2))
+	r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(hs.SignUpStep2))
 
 	// invited
 	r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode))
-	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite))
+	r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(hs.CompleteInvite))
 
 	// reset password
 	r.Get("/user/password/send-reset-email", hs.Index)
@@ -109,7 +109,7 @@ func (hs *HTTPServer) registerRoutes() {
 	r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot))
 
 	// api renew session based on remember cookie
-	r.Get("/api/login/ping", quota("session"), LoginAPIPing)
+	r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing)
 
 	// authed api
 	r.Group("/api", func(apiRoute routing.RouteRegister) {

+ 32 - 10
pkg/api/common_test.go

@@ -5,7 +5,6 @@ import (
 	"net/http/httptest"
 	"path/filepath"
 
-	"github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -95,13 +94,14 @@ func (sc *scenarioContext) fakeReqWithParams(method, url string, queryParams map
 }
 
 type scenarioContext struct {
-	m              *macaron.Macaron
-	context        *m.ReqContext
-	resp           *httptest.ResponseRecorder
-	handlerFunc    handlerFunc
-	defaultHandler macaron.Handler
-	req            *http.Request
-	url            string
+	m                    *macaron.Macaron
+	context              *m.ReqContext
+	resp                 *httptest.ResponseRecorder
+	handlerFunc          handlerFunc
+	defaultHandler       macaron.Handler
+	req                  *http.Request
+	url                  string
+	userAuthTokenService *fakeUserAuthTokenService
 }
 
 func (sc *scenarioContext) exec() {
@@ -123,8 +123,30 @@ func setupScenarioContext(url string) *scenarioContext {
 		Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 	}))
 
-	sc.m.Use(middleware.GetContextHandler())
-	sc.m.Use(middleware.Sessioner(&session.Options{}, 0))
+	sc.userAuthTokenService = newFakeUserAuthTokenService()
+	sc.m.Use(middleware.GetContextHandler(sc.userAuthTokenService))
 
 	return sc
 }
+
+type fakeUserAuthTokenService struct {
+	initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+	return &fakeUserAuthTokenService{
+		initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+			return false
+		},
+	}
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+	return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+	return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}

+ 1 - 1
pkg/api/dashboard.go

@@ -336,7 +336,7 @@ func addGettingStartedPanelToHomeDashboard(dash *simplejson.Json) {
 		"id":   123123,
 		"gridPos": map[string]interface{}{
 			"x": 0,
-			"y": 0,
+			"y": 3,
 			"w": 24,
 			"h": 4,
 		},

+ 2 - 0
pkg/api/dtos/alerting.go

@@ -50,6 +50,7 @@ func formatShort(interval time.Duration) string {
 func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
 	return &AlertNotification{
 		Id:                    notification.Id,
+		Uid:                   notification.Uid,
 		Name:                  notification.Name,
 		Type:                  notification.Type,
 		IsDefault:             notification.IsDefault,
@@ -64,6 +65,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica
 
 type AlertNotification struct {
 	Id                    int64            `json:"id"`
+	Uid                   string           `json:"uid"`
 	Name                  string           `json:"name"`
 	Type                  string           `json:"type"`
 	IsDefault             bool             `json:"isDefault"`

+ 17 - 16
pkg/api/http_server.go

@@ -11,14 +11,8 @@ import (
 	"path"
 	"time"
 
-	"github.com/grafana/grafana/pkg/api/routing"
-	"github.com/prometheus/client_golang/prometheus"
-
-	"github.com/prometheus/client_golang/prometheus/promhttp"
-
-	macaron "gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/api/live"
+	"github.com/grafana/grafana/pkg/api/routing"
 	httpstatic "github.com/grafana/grafana/pkg/api/static"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
@@ -27,11 +21,16 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/plugins"
 	"github.com/grafana/grafana/pkg/registry"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/cache"
 	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/hooks"
 	"github.com/grafana/grafana/pkg/services/rendering"
+	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/prometheus/client_golang/prometheus"
+	"github.com/prometheus/client_golang/prometheus/promhttp"
+	macaron "gopkg.in/macaron.v1"
 )
 
 func init() {
@@ -49,13 +48,14 @@ type HTTPServer struct {
 	streamManager *live.StreamManager
 	httpSrv       *http.Server
 
-	RouteRegister   routing.RouteRegister    `inject:""`
-	Bus             bus.Bus                  `inject:""`
-	RenderService   rendering.Service        `inject:""`
-	Cfg             *setting.Cfg             `inject:""`
-	HooksService    *hooks.HooksService      `inject:""`
-	CacheService    *cache.CacheService      `inject:""`
-	DatasourceCache datasources.CacheService `inject:""`
+	RouteRegister    routing.RouteRegister     `inject:""`
+	Bus              bus.Bus                   `inject:""`
+	RenderService    rendering.Service         `inject:""`
+	Cfg              *setting.Cfg              `inject:""`
+	HooksService     *hooks.HooksService       `inject:""`
+	CacheService     *cache.CacheService       `inject:""`
+	DatasourceCache  datasources.CacheService  `inject:""`
+	AuthTokenService auth.UserAuthTokenService `inject:""`
 }
 
 func (hs *HTTPServer) Init() error {
@@ -65,6 +65,8 @@ func (hs *HTTPServer) Init() error {
 	hs.macaron = hs.newMacaron()
 	hs.registerRoutes()
 
+	session.Init(&setting.SessionOptions, setting.SessionConnMaxLifetime)
+
 	return nil
 }
 
@@ -223,8 +225,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
 
 	m.Use(hs.healthHandler)
 	m.Use(hs.metricsEndpoint)
-	m.Use(middleware.GetContextHandler())
-	m.Use(middleware.Sessioner(&setting.SessionOptions, setting.SessionConnMaxLifetime))
+	m.Use(middleware.GetContextHandler(hs.AuthTokenService))
 	m.Use(middleware.OrgRedirect())
 
 	// needs to be after context handler

+ 59 - 67
pkg/api/login.go

@@ -1,6 +1,8 @@
 package api
 
 import (
+	"encoding/hex"
+	"net/http"
 	"net/url"
 
 	"github.com/grafana/grafana/pkg/api/dtos"
@@ -9,12 +11,13 @@ import (
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/util"
 )
 
 const (
-	ViewIndex = "index"
+	ViewIndex            = "index"
+	LoginErrorCookieName = "login_error"
 )
 
 func (hs *HTTPServer) LoginView(c *m.ReqContext) {
@@ -34,8 +37,8 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 	viewData.Settings["loginHint"] = setting.LoginHint
 	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 
-	if loginError, ok := c.Session.Get("loginError").(string); ok {
-		c.Session.Delete("loginError")
+	if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {
+		deleteCookie(c, LoginErrorCookieName)
 		viewData.Settings["loginError"] = loginError
 	}
 
@@ -43,7 +46,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 		return
 	}
 
-	if !tryLoginUsingRememberCookie(c) {
+	if !c.IsSignedIn {
 		c.HTML(200, ViewIndex, viewData)
 		return
 	}
@@ -75,56 +78,15 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool {
 	return false
 }
 
-func tryLoginUsingRememberCookie(c *m.ReqContext) bool {
-	// Check auto-login.
-	uname := c.GetCookie(setting.CookieUserName)
-	if len(uname) == 0 {
-		return false
-	}
-
-	isSucceed := false
-	defer func() {
-		if !isSucceed {
-			log.Trace("auto-login cookie cleared: %s", uname)
-			c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
-			c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
-			return
-		}
-	}()
-
-	userQuery := m.GetUserByLoginQuery{LoginOrEmail: uname}
-	if err := bus.Dispatch(&userQuery); err != nil {
-		return false
-	}
-
-	user := userQuery.Result
-
-	// validate remember me cookie
-	signingKey := user.Rands + user.Password
-	if len(signingKey) < 10 {
-		c.Logger.Error("Invalid user signingKey")
-		return false
+func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response {
+	if c.IsSignedIn || c.IsAnonymous {
+		return JSON(200, "Logged in")
 	}
 
-	if val, _ := c.GetSuperSecureCookie(signingKey, setting.CookieRememberName); val != user.Login {
-		return false
-	}
-
-	isSucceed = true
-	loginUserWithUser(user, c)
-	return true
+	return Error(401, "Unauthorized", nil)
 }
 
-func LoginAPIPing(c *m.ReqContext) {
-	if !tryLoginUsingRememberCookie(c) {
-		c.JsonApiErr(401, "Unauthorized", nil)
-		return
-	}
-
-	c.JsonOK("Logged in")
-}
-
-func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
+func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 	if setting.DisableLoginForm {
 		return Error(401, "Login is disabled", nil)
 	}
@@ -146,7 +108,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 
 	user := authQuery.User
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 
 	result := map[string]interface{}{
 		"message": "Logged in",
@@ -162,30 +124,60 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 	return JSON(200, result)
 }
 
-func loginUserWithUser(user *m.User, c *m.ReqContext) {
+func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) {
 	if user == nil {
-		log.Error(3, "User login with nil user")
+		hs.log.Error("User login with nil user")
 	}
 
-	c.Resp.Header().Del("Set-Cookie")
-
-	days := 86400 * setting.LogInRememberDays
-	if days > 0 {
-		c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/")
-		c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/")
+	err := hs.AuthTokenService.UserAuthenticatedHook(user, c)
+	if err != nil {
+		hs.log.Error("User auth hook failed", "error", err)
 	}
-
-	c.Session.RegenerateId(c.Context)
-	c.Session.Set(session.SESS_KEY_USERID, user.Id)
 }
 
-func Logout(c *m.ReqContext) {
-	c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/")
-	c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/")
-	c.Session.Destory(c.Context)
+func (hs *HTTPServer) Logout(c *m.ReqContext) {
+	hs.AuthTokenService.UserSignedOutHook(c)
+
 	if setting.SignoutRedirectUrl != "" {
 		c.Redirect(setting.SignoutRedirectUrl)
 	} else {
 		c.Redirect(setting.AppSubUrl + "/login")
 	}
 }
+
+func tryGetEncryptedCookie(ctx *m.ReqContext, cookieName string) (string, bool) {
+	cookie := ctx.GetCookie(cookieName)
+	if cookie == "" {
+		return "", false
+	}
+
+	decoded, err := hex.DecodeString(cookie)
+	if err != nil {
+		return "", false
+	}
+
+	decryptedError, err := util.Decrypt([]byte(decoded), setting.SecretKey)
+	return string(decryptedError), err == nil
+}
+
+func deleteCookie(ctx *m.ReqContext, cookieName string) {
+	ctx.SetCookie(cookieName, "", -1, setting.AppSubUrl+"/")
+}
+
+func (hs *HTTPServer) trySetEncryptedCookie(ctx *m.ReqContext, cookieName string, value string, maxAge int) error {
+	encryptedError, err := util.Encrypt([]byte(value), setting.SecretKey)
+	if err != nil {
+		return err
+	}
+
+	http.SetCookie(ctx.Resp, &http.Cookie{
+		Name:     cookieName,
+		MaxAge:   60,
+		Value:    hex.EncodeToString(encryptedError),
+		HttpOnly: true,
+		Path:     setting.AppSubUrl + "/",
+		Secure:   hs.Cfg.SecurityHTTPSCookies,
+	})
+
+	return nil
+}

+ 49 - 16
pkg/api/login_oauth.go

@@ -3,9 +3,11 @@ package api
 import (
 	"context"
 	"crypto/rand"
+	"crypto/sha256"
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
+	"encoding/hex"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -18,12 +20,14 @@ import (
 	"github.com/grafana/grafana/pkg/login"
 	"github.com/grafana/grafana/pkg/metrics"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/social"
 )
 
-var oauthLogger = log.New("oauth")
+var (
+	oauthLogger          = log.New("oauth")
+	OauthStateCookieName = "oauth_state"
+)
 
 func GenStateString() string {
 	rnd := make([]byte, 32)
@@ -31,7 +35,7 @@ func GenStateString() string {
 	return base64.URLEncoding.EncodeToString(rnd)
 }
 
-func OAuthLogin(ctx *m.ReqContext) {
+func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) {
 	if setting.OAuthService == nil {
 		ctx.Handle(404, "OAuth not enabled", nil)
 		return
@@ -48,14 +52,15 @@ func OAuthLogin(ctx *m.ReqContext) {
 	if errorParam != "" {
 		errorDesc := ctx.Query("error_description")
 		oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc)
-		redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
+		hs.redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc)
 		return
 	}
 
 	code := ctx.Query("code")
 	if code == "" {
 		state := GenStateString()
-		ctx.Session.Set(session.SESS_KEY_OAUTH_STATE, state)
+		hashedState := hashStatecode(state, setting.OAuthService.OAuthInfos[name].ClientSecret)
+		hs.writeCookie(ctx.Resp, OauthStateCookieName, hashedState, 60)
 		if setting.OAuthService.OAuthInfos[name].HostedDomain == "" {
 			ctx.Redirect(connect.AuthCodeURL(state, oauth2.AccessTypeOnline))
 		} else {
@@ -64,14 +69,20 @@ func OAuthLogin(ctx *m.ReqContext) {
 		return
 	}
 
-	savedState, ok := ctx.Session.Get(session.SESS_KEY_OAUTH_STATE).(string)
-	if !ok {
+	cookieState := ctx.GetCookie(OauthStateCookieName)
+
+	// delete cookie
+	ctx.Resp.Header().Del("Set-Cookie")
+	hs.deleteCookie(ctx.Resp, OauthStateCookieName)
+
+	if cookieState == "" {
 		ctx.Handle(500, "login.OAuthLogin(missing saved state)", nil)
 		return
 	}
 
-	queryState := ctx.Query("state")
-	if savedState != queryState {
+	queryState := hashStatecode(ctx.Query("state"), setting.OAuthService.OAuthInfos[name].ClientSecret)
+	oauthLogger.Info("state check", "queryState", queryState, "cookieState", cookieState)
+	if cookieState != queryState {
 		ctx.Handle(500, "login.OAuthLogin(state mismatch)", nil)
 		return
 	}
@@ -131,7 +142,7 @@ func OAuthLogin(ctx *m.ReqContext) {
 	userInfo, err := connect.UserInfo(client, token)
 	if err != nil {
 		if sErr, ok := err.(*social.Error); ok {
-			redirectWithError(ctx, sErr)
+			hs.redirectWithError(ctx, sErr)
 		} else {
 			ctx.Handle(500, fmt.Sprintf("login.OAuthLogin(get info from %s)", name), err)
 		}
@@ -142,13 +153,13 @@ func OAuthLogin(ctx *m.ReqContext) {
 
 	// validate that we got at least an email address
 	if userInfo.Email == "" {
-		redirectWithError(ctx, login.ErrNoEmail)
+		hs.redirectWithError(ctx, login.ErrNoEmail)
 		return
 	}
 
 	// validate that the email is allowed to login to grafana
 	if !connect.IsEmailAllowed(userInfo.Email) {
-		redirectWithError(ctx, login.ErrEmailNotAllowed)
+		hs.redirectWithError(ctx, login.ErrEmailNotAllowed)
 		return
 	}
 
@@ -171,14 +182,15 @@ func OAuthLogin(ctx *m.ReqContext) {
 		ExternalUser:  extUser,
 		SignupAllowed: connect.IsSignupAllowed(),
 	}
+
 	err = bus.Dispatch(cmd)
 	if err != nil {
-		redirectWithError(ctx, err)
+		hs.redirectWithError(ctx, err)
 		return
 	}
 
 	// login
-	loginUserWithUser(cmd.Result, ctx)
+	hs.loginUserWithUser(cmd.Result, ctx)
 
 	metrics.M_Api_Login_OAuth.Inc()
 
@@ -191,8 +203,29 @@ func OAuthLogin(ctx *m.ReqContext) {
 	ctx.Redirect(setting.AppSubUrl + "/")
 }
 
-func redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
+func (hs *HTTPServer) deleteCookie(w http.ResponseWriter, name string) {
+	hs.writeCookie(w, name, "", -1)
+}
+
+func (hs *HTTPServer) writeCookie(w http.ResponseWriter, name string, value string, maxAge int) {
+	http.SetCookie(w, &http.Cookie{
+		Name:     name,
+		MaxAge:   maxAge,
+		Value:    value,
+		HttpOnly: true,
+		Path:     setting.AppSubUrl + "/",
+		Secure:   hs.Cfg.SecurityHTTPSCookies,
+	})
+}
+
+func hashStatecode(code, seed string) string {
+	hashBytes := sha256.Sum256([]byte(code + setting.SecretKey + seed))
+	return hex.EncodeToString(hashBytes[:])
+}
+
+func (hs *HTTPServer) redirectWithError(ctx *m.ReqContext, err error, v ...interface{}) {
 	ctx.Logger.Error(err.Error(), v...)
-	ctx.Session.Set("loginError", err.Error())
+	hs.trySetEncryptedCookie(ctx, LoginErrorCookieName, err.Error(), 60)
+
 	ctx.Redirect(setting.AppSubUrl + "/login")
 }

+ 2 - 2
pkg/api/org_invite.go

@@ -148,7 +148,7 @@ func GetInviteInfoByCode(c *m.ReqContext) Response {
 	})
 }
 
-func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
+func (hs *HTTPServer) CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response {
 	query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode}
 
 	if err := bus.Dispatch(&query); err != nil {
@@ -186,7 +186,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res
 		return rsp
 	}
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 
 	metrics.M_Api_User_SignUpCompleted.Inc()
 	metrics.M_Api_User_SignUpInvite.Inc()

+ 1 - 1
pkg/api/pluginproxy/ds_proxy.go

@@ -54,7 +54,7 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
 
 func newHTTPClient() httpClient {
 	return &http.Client{
-		Timeout:   time.Second * 30,
+		Timeout:   time.Duration(setting.DataProxyTimeout) * time.Second,
 		Transport: &http.Transport{Proxy: http.ProxyFromEnvironment},
 	}
 }

+ 2 - 2
pkg/api/signup.go

@@ -51,7 +51,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response {
 	return JSON(200, util.DynMap{"status": "SignUpCreated"})
 }
 
-func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
+func (hs *HTTPServer) SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 	if !setting.AllowUserSignUp {
 		return Error(401, "User signup is disabled", nil)
 	}
@@ -109,7 +109,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response {
 		apiResponse["code"] = "redirect-to-select-org"
 	}
 
-	loginUserWithUser(user, c)
+	hs.loginUserWithUser(user, c)
 	metrics.M_Api_User_SignUpCompleted.Inc()
 
 	return JSON(200, apiResponse)

+ 0 - 11
pkg/middleware/auth.go

@@ -7,7 +7,6 @@ import (
 	"gopkg.in/macaron.v1"
 
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 )
@@ -17,16 +16,6 @@ type AuthOptions struct {
 	ReqSignedIn     bool
 }
 
-func getRequestUserId(c *m.ReqContext) int64 {
-	userID := c.Session.Get(session.SESS_KEY_USERID)
-
-	if userID != nil {
-		return userID.(int64)
-	}
-
-	return 0
-}
-
 func getApiKey(c *m.ReqContext) string {
 	header := c.Req.Header.Get("Authorization")
 	parts := strings.SplitN(header, " ", 2)

+ 19 - 1
pkg/middleware/auth_proxy.go

@@ -16,7 +16,9 @@ import (
 	"github.com/grafana/grafana/pkg/setting"
 )
 
-var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+var (
+	AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue"
+)
 
 func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 	if !setting.AuthProxyEnabled {
@@ -40,6 +42,12 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool {
 		return false
 	}
 
+	defer func() {
+		if err := ctx.Session.Release(); err != nil {
+			ctx.Logger.Error("failed to save session data", "error", err)
+		}
+	}()
+
 	query := &m.GetSignedInUserQuery{OrgId: orgID}
 
 	// if this session has already been authenticated by authProxy just load the user
@@ -192,6 +200,16 @@ var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error {
 	return nil
 }
 
+func getRequestUserId(c *m.ReqContext) int64 {
+	userID := c.Session.Get(session.SESS_KEY_USERID)
+
+	if userID != nil {
+		return userID.(int64)
+	}
+
+	return 0
+}
+
 func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error {
 	if len(strings.TrimSpace(setting.AuthProxyWhitelist)) == 0 {
 		return nil

+ 5 - 28
pkg/middleware/middleware.go

@@ -3,15 +3,15 @@ package middleware
 import (
 	"strconv"
 
-	"gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/apikeygen"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/auth"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
+	macaron "gopkg.in/macaron.v1"
 )
 
 var (
@@ -21,12 +21,12 @@ var (
 	ReqOrgAdmin     = RoleAuth(m.ROLE_ADMIN)
 )
 
-func GetContextHandler() macaron.Handler {
+func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler {
 	return func(c *macaron.Context) {
 		ctx := &m.ReqContext{
 			Context:        c,
 			SignedInUser:   &m.SignedInUser{},
-			Session:        session.GetSession(),
+			Session:        session.GetSession(), // should only be used by auth_proxy
 			IsSignedIn:     false,
 			AllowAnonymous: false,
 			SkipCache:      false,
@@ -49,7 +49,7 @@ func GetContextHandler() macaron.Handler {
 		case initContextWithApiKey(ctx):
 		case initContextWithBasicAuth(ctx, orgId):
 		case initContextWithAuthProxy(ctx, orgId):
-		case initContextWithUserSessionCookie(ctx, orgId):
+		case ats.InitContextWithToken(ctx, orgId):
 		case initContextWithAnonymousUser(ctx):
 		}
 
@@ -88,29 +88,6 @@ func initContextWithAnonymousUser(ctx *m.ReqContext) bool {
 	return true
 }
 
-func initContextWithUserSessionCookie(ctx *m.ReqContext, orgId int64) bool {
-	// initialize session
-	if err := ctx.Session.Start(ctx.Context); err != nil {
-		ctx.Logger.Error("Failed to start session", "error", err)
-		return false
-	}
-
-	var userId int64
-	if userId = getRequestUserId(ctx); userId == 0 {
-		return false
-	}
-
-	query := m.GetSignedInUserQuery{UserId: userId, OrgId: orgId}
-	if err := bus.Dispatch(&query); err != nil {
-		ctx.Logger.Error("Failed to get user with id", "userId", userId, "error", err)
-		return false
-	}
-
-	ctx.SignedInUser = query.Result
-	ctx.IsSignedIn = true
-	return true
-}
-
 func initContextWithApiKey(ctx *m.ReqContext) bool {
 	var keyString string
 	if keyString = getApiKey(ctx); keyString == "" {

+ 48 - 30
pkg/middleware/middleware_test.go

@@ -7,7 +7,7 @@ import (
 	"path/filepath"
 	"testing"
 
-	ms "github.com/go-macaron/session"
+	msession "github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
@@ -43,11 +43,6 @@ func TestMiddlewareContext(t *testing.T) {
 			So(sc.resp.Header().Get("Cache-Control"), ShouldBeEmpty)
 		})
 
-		middlewareScenario("Non api request should init session", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").exec()
-			So(sc.resp.Header().Get("Set-Cookie"), ShouldContainSubstring, "grafana_sess")
-		})
-
 		middlewareScenario("Invalid api key", func(sc *scenarioContext) {
 			sc.apiKey = "invalid_key_test"
 			sc.fakeReq("GET", "/").exec()
@@ -151,22 +146,17 @@ func TestMiddlewareContext(t *testing.T) {
 			})
 		})
 
-		middlewareScenario("UserId in session", func(sc *scenarioContext) {
-
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
-				return nil
-			})
+		middlewareScenario("Auth token service", func(sc *scenarioContext) {
+			var wasCalled bool
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				wasCalled = true
+				return false
+			}
 
 			sc.fakeReq("GET", "/").exec()
 
-			Convey("should init context with user info", func() {
-				So(sc.context.IsSignedIn, ShouldBeTrue)
-				So(sc.context.UserId, ShouldEqual, 12)
+			Convey("should call middleware", func() {
+				So(wasCalled, ShouldBeTrue)
 			})
 		})
 
@@ -211,6 +201,7 @@ func TestMiddlewareContext(t *testing.T) {
 				return nil
 			})
 
+			setting.SessionOptions = msession.Options{}
 			sc.fakeReq("GET", "/")
 			sc.req.Header.Add("X-WEBAUTH-USER", "torkelo")
 			sc.exec()
@@ -479,6 +470,7 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 		defer bus.ClearBusHandlers()
 
 		sc := &scenarioContext{}
+
 		viewsPath, _ := filepath.Abs("../../public/views")
 
 		sc.m = macaron.New()
@@ -487,10 +479,13 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		}))
 
-		sc.m.Use(GetContextHandler())
+		session.Init(&msession.Options{}, 0)
+		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}, 0))
+		setting.SessionOptions = msession.Options{}
+
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 
@@ -508,15 +503,16 @@ func middlewareScenario(desc string, fn scenarioFunc) {
 }
 
 type scenarioContext struct {
-	m              *macaron.Macaron
-	context        *m.ReqContext
-	resp           *httptest.ResponseRecorder
-	apiKey         string
-	authHeader     string
-	respJson       map[string]interface{}
-	handlerFunc    handlerFunc
-	defaultHandler macaron.Handler
-	url            string
+	m                    *macaron.Macaron
+	context              *m.ReqContext
+	resp                 *httptest.ResponseRecorder
+	apiKey               string
+	authHeader           string
+	respJson             map[string]interface{}
+	handlerFunc          handlerFunc
+	defaultHandler       macaron.Handler
+	url                  string
+	userAuthTokenService *fakeUserAuthTokenService
 
 	req *http.Request
 }
@@ -585,3 +581,25 @@ func (sc *scenarioContext) exec() {
 
 type scenarioFunc func(c *scenarioContext)
 type handlerFunc func(c *m.ReqContext)
+
+type fakeUserAuthTokenService struct {
+	initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool
+}
+
+func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
+	return &fakeUserAuthTokenService{
+		initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool {
+			return false
+		},
+	}
+}
+
+func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool {
+	return s.initContextWithTokenProvider(ctx, orgID)
+}
+
+func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error {
+	return nil
+}
+
+func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {}

+ 0 - 1
pkg/middleware/org_redirect.go

@@ -9,7 +9,6 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
-
 	"gopkg.in/macaron.v1"
 )
 

+ 11 - 13
pkg/middleware/org_redirect_test.go

@@ -7,7 +7,6 @@ import (
 
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -15,18 +14,15 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 
 	Convey("Can redirect to correct org", t, func() {
 		middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 				return nil
 			})
 
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
-				return nil
-			})
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
 
 			sc.m.Get("/", sc.defaultHandler)
 			sc.fakeReq("GET", "/?orgId=3").exec()
@@ -37,14 +33,16 @@ func TestOrgRedirectMiddleware(t *testing.T) {
 		})
 
 		middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) {
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
-
 			bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error {
 				return fmt.Errorf("")
 			})
 
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
+
 			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
 				query.Result = &m.SignedInUser{OrgId: 1, UserId: 12}
 				return nil

+ 5 - 8
pkg/middleware/quota_test.go

@@ -74,15 +74,12 @@ func TestMiddlewareQuota(t *testing.T) {
 		})
 
 		middlewareScenario("with user logged in", func(sc *scenarioContext) {
-			// log us in, so we have a user_id and org_id in the context
-			sc.fakeReq("GET", "/").handler(func(c *m.ReqContext) {
-				c.Session.Set(session.SESS_KEY_USERID, int64(12))
-			}).exec()
+			sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool {
+				ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12}
+				ctx.IsSignedIn = true
+				return true
+			}
 
-			bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error {
-				query.Result = &m.SignedInUser{OrgId: 2, UserId: 12}
-				return nil
-			})
 			bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
 				query.Result = &m.GlobalQuotaDTO{
 					Target: query.Target,

+ 3 - 4
pkg/middleware/recovery_test.go

@@ -4,13 +4,12 @@ import (
 	"path/filepath"
 	"testing"
 
-	ms "github.com/go-macaron/session"
 	"github.com/grafana/grafana/pkg/bus"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
 	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/macaron.v1"
+	macaron "gopkg.in/macaron.v1"
 )
 
 func TestRecoveryMiddleware(t *testing.T) {
@@ -64,10 +63,10 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
 			Delims:    macaron.Delims{Left: "[[", Right: "]]"},
 		}))
 
-		sc.m.Use(GetContextHandler())
+		sc.userAuthTokenService = newFakeUserAuthTokenService()
+		sc.m.Use(GetContextHandler(sc.userAuthTokenService))
 		// mock out gc goroutine
 		session.StartSessionGC = func() {}
-		sc.m.Use(Sessioner(&ms.Options{}, 0))
 		sc.m.Use(OrgRedirect())
 		sc.m.Use(AddDefaultResponseHeaders())
 

+ 0 - 21
pkg/middleware/session.go

@@ -1,21 +0,0 @@
-package middleware
-
-import (
-	ms "github.com/go-macaron/session"
-	"gopkg.in/macaron.v1"
-
-	m "github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/session"
-)
-
-func Sessioner(options *ms.Options, sessionConnMaxLifetime int64) macaron.Handler {
-	session.Init(options, sessionConnMaxLifetime)
-
-	return func(ctx *m.ReqContext) {
-		ctx.Next()
-
-		if err := ctx.Session.Release(); err != nil {
-			panic("session(release): " + err.Error())
-		}
-	}
-}

+ 34 - 6
pkg/models/alert_notifications.go

@@ -8,10 +8,11 @@ import (
 )
 
 var (
-	ErrNotificationFrequencyNotFound         = errors.New("Notification frequency not specified")
-	ErrAlertNotificationStateNotFound        = errors.New("alert notification state not found")
-	ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
-	ErrAlertNotificationStateAlreadyExist    = errors.New("alert notification state already exists.")
+	ErrNotificationFrequencyNotFound            = errors.New("Notification frequency not specified")
+	ErrAlertNotificationStateNotFound           = errors.New("alert notification state not found")
+	ErrAlertNotificationStateVersionConflict    = errors.New("alert notification state update version conflict")
+	ErrAlertNotificationStateAlreadyExist       = errors.New("alert notification state already exists.")
+	ErrAlertNotificationFailedGenerateUniqueUid = errors.New("Failed to generate unique alert notification uid")
 )
 
 type AlertNotificationStateType string
@@ -24,6 +25,7 @@ var (
 
 type AlertNotification struct {
 	Id                    int64            `json:"id"`
+	Uid                   string           `json:"-"`
 	OrgId                 int64            `json:"-"`
 	Name                  string           `json:"name"`
 	Type                  string           `json:"type"`
@@ -37,6 +39,7 @@ type AlertNotification struct {
 }
 
 type CreateAlertNotificationCommand struct {
+	Uid                   string           `json:"-"`
 	Name                  string           `json:"name"  binding:"Required"`
 	Type                  string           `json:"type"  binding:"Required"`
 	SendReminder          bool             `json:"sendReminder"`
@@ -63,10 +66,28 @@ type UpdateAlertNotificationCommand struct {
 	Result *AlertNotification
 }
 
+type UpdateAlertNotificationWithUidCommand struct {
+	Uid                   string
+	Name                  string
+	Type                  string
+	SendReminder          bool
+	DisableResolveMessage bool
+	Frequency             string
+	IsDefault             bool
+	Settings              *simplejson.Json
+
+	OrgId  int64
+	Result *AlertNotification
+}
+
 type DeleteAlertNotificationCommand struct {
 	Id    int64
 	OrgId int64
 }
+type DeleteAlertNotificationWithUidCommand struct {
+	Uid   string
+	OrgId int64
+}
 
 type GetAlertNotificationsQuery struct {
 	Name  string
@@ -76,8 +97,15 @@ type GetAlertNotificationsQuery struct {
 	Result *AlertNotification
 }
 
-type GetAlertNotificationsToSendQuery struct {
-	Ids   []int64
+type GetAlertNotificationsWithUidQuery struct {
+	Uid   string
+	OrgId int64
+
+	Result *AlertNotification
+}
+
+type GetAlertNotificationsWithUidToSendQuery struct {
+	Uids  []string
 	OrgId int64
 
 	Result []*AlertNotification

+ 3 - 3
pkg/models/context.go

@@ -3,18 +3,18 @@ package models
 import (
 	"strings"
 
-	"github.com/prometheus/client_golang/prometheus"
-	"gopkg.in/macaron.v1"
-
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/services/session"
 	"github.com/grafana/grafana/pkg/setting"
+	"github.com/prometheus/client_golang/prometheus"
+	"gopkg.in/macaron.v1"
 )
 
 type ReqContext struct {
 	*macaron.Context
 	*SignedInUser
 
+	// This should only be used by the auth_proxy
 	Session session.SessionStore
 
 	IsSignedIn     bool

+ 13 - 3
pkg/services/alerting/engine.go

@@ -105,8 +105,9 @@ func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error {
 var (
 	unfinishedWorkTimeout = time.Second * 5
 	// TODO: Make alertTimeout and alertMaxAttempts configurable in the config file.
-	alertTimeout     = time.Second * 30
-	alertMaxAttempts = 3
+	alertTimeout        = time.Second * 30
+	resultHandleTimeout = time.Second * 30
+	alertMaxAttempts    = 3
 )
 
 func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error {
@@ -116,7 +117,7 @@ func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *J
 		}
 	}()
 
-	cancelChan := make(chan context.CancelFunc, alertMaxAttempts)
+	cancelChan := make(chan context.CancelFunc, alertMaxAttempts*2)
 	attemptChan := make(chan int, 1)
 
 	// Initialize with first attemptID=1
@@ -204,6 +205,15 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel
 			}
 		}
 
+		// create new context with timeout for notifications
+		resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), resultHandleTimeout)
+		cancelChan <- resultHandleCancelFn
+
+		// override the context used for evaluation with a new context for notifications.
+		// This makes it possible for notifiers to execute when datasources
+		// dont respond within the timeout limit. We should rewrite this so notifications
+		// dont reuse the evalContext and get its own context.
+		evalContext.Ctx = resultHandleCtx
 		evalContext.Rule.State = evalContext.GetNewState()
 		e.resultHandler.Handle(evalContext)
 		span.Finish()

+ 148 - 0
pkg/services/alerting/engine_integration_test.go

@@ -0,0 +1,148 @@
+// +build integration
+
+package alerting
+
+import (
+	"context"
+	"errors"
+	"net"
+	"net/http"
+	"net/http/httptest"
+	"testing"
+	"time"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestEngineTimeouts(t *testing.T) {
+	Convey("Alerting engine timeout tests", t, func() {
+		engine := NewEngine()
+		engine.resultHandler = &FakeResultHandler{}
+		job := &Job{Running: true, Rule: &Rule{}}
+
+		Convey("Should trigger as many retries as needed", func() {
+			Convey("pended alert for datasource -> result handler should be worked", func() {
+				// reduce alert timeout to test quickly
+				originAlertTimeout := alertTimeout
+				alertTimeout = 2 * time.Second
+				transportTimeoutInterval := 2 * time.Second
+				serverBusySleepDuration := 1 * time.Second
+
+				evalHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
+				resultHandler := NewFakeCommonTimeoutHandler(transportTimeoutInterval, serverBusySleepDuration)
+				engine.evalHandler = evalHandler
+				engine.resultHandler = resultHandler
+
+				engine.processJobWithRetry(context.TODO(), job)
+
+				So(evalHandler.EvalSucceed, ShouldEqual, true)
+				So(resultHandler.ResultHandleSucceed, ShouldEqual, true)
+
+				// initialize for other tests.
+				alertTimeout = originAlertTimeout
+				engine.resultHandler = &FakeResultHandler{}
+			})
+		})
+	})
+}
+
+type FakeCommonTimeoutHandler struct {
+	TransportTimeoutDuration time.Duration
+	ServerBusySleepDuration  time.Duration
+	EvalSucceed              bool
+	ResultHandleSucceed      bool
+}
+
+func NewFakeCommonTimeoutHandler(transportTimeoutDuration time.Duration, serverBusySleepDuration time.Duration) *FakeCommonTimeoutHandler {
+	return &FakeCommonTimeoutHandler{
+		TransportTimeoutDuration: transportTimeoutDuration,
+		ServerBusySleepDuration:  serverBusySleepDuration,
+		EvalSucceed:              false,
+		ResultHandleSucceed:      false,
+	}
+}
+
+func (handler *FakeCommonTimeoutHandler) Eval(evalContext *EvalContext) {
+	// 1. prepare mock server
+	path := "/evaltimeout"
+	srv := runBusyServer(path, handler.ServerBusySleepDuration)
+	defer srv.Close()
+
+	// 2. send requests
+	url := srv.URL + path
+	res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
+	if res != nil {
+		defer res.Body.Close()
+	}
+
+	if err != nil {
+		evalContext.Error = errors.New("Fake evaluation timeout test failure")
+		return
+	}
+
+	if res.StatusCode == 200 {
+		handler.EvalSucceed = true
+	}
+
+	evalContext.Error = errors.New("Fake evaluation timeout test failure; wrong response")
+}
+
+func (handler *FakeCommonTimeoutHandler) Handle(evalContext *EvalContext) error {
+	// 1. prepare mock server
+	path := "/resulthandle"
+	srv := runBusyServer(path, handler.ServerBusySleepDuration)
+	defer srv.Close()
+
+	// 2. send requests
+	url := srv.URL + path
+	res, err := sendRequest(evalContext.Ctx, url, handler.TransportTimeoutDuration)
+	if res != nil {
+		defer res.Body.Close()
+	}
+
+	if err != nil {
+		evalContext.Error = errors.New("Fake result handle timeout test failure")
+		return evalContext.Error
+	}
+
+	if res.StatusCode == 200 {
+		handler.ResultHandleSucceed = true
+		return nil
+	}
+
+	evalContext.Error = errors.New("Fake result handle timeout test failure; wrong response")
+
+	return evalContext.Error
+}
+
+func runBusyServer(path string, serverBusySleepDuration time.Duration) *httptest.Server {
+	mux := http.NewServeMux()
+	server := httptest.NewServer(mux)
+
+	mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(serverBusySleepDuration)
+	})
+
+	return server
+}
+
+func sendRequest(context context.Context, url string, transportTimeoutInterval time.Duration) (resp *http.Response, err error) {
+	req, err := http.NewRequest("GET", url, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	req = req.WithContext(context)
+
+	transport := http.Transport{
+		Dial: (&net.Dialer{
+			Timeout:   transportTimeoutInterval,
+			KeepAlive: transportTimeoutInterval,
+		}).Dial,
+	}
+	client := http.Client{
+		Transport: &transport,
+	}
+
+	return client.Do(req)
+}

+ 57 - 46
pkg/services/alerting/extractor_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	m "github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/services/sqlstore"
 	. "github.com/smartystreets/goconvey/convey"
 )
 
@@ -197,74 +198,84 @@ func TestAlertRuleExtraction(t *testing.T) {
 			})
 		})
 
-		Convey("Parse and validate dashboard containing influxdb alert", func() {
-			json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
+		Convey("Alert notifications are in DB", func() {
+			sqlstore.InitTestDB(t)
+			firstNotification := m.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
+			err = sqlstore.CreateAlertNotificationCommand(&firstNotification)
 			So(err, ShouldBeNil)
-
-			dashJson, err := simplejson.NewJson(json)
+			secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
+			err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
 			So(err, ShouldBeNil)
-			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
 
-			alerts, err := extractor.GetAlerts()
+			Convey("Parse and validate dashboard containing influxdb alert", func() {
+				json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
+				So(err, ShouldBeNil)
 
-			Convey("Get rules without error", func() {
+				dashJson, err := simplejson.NewJson(json)
 				So(err, ShouldBeNil)
-			})
+				dash := m.NewDashboardFromJson(dashJson)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
-			Convey("should be able to read interval", func() {
-				So(len(alerts), ShouldEqual, 1)
+				alerts, err := extractor.GetAlerts()
 
-				for _, alert := range alerts {
-					So(alert.DashboardId, ShouldEqual, 4)
+				Convey("Get rules without error", func() {
+					So(err, ShouldBeNil)
+				})
 
-					conditions := alert.Settings.Get("conditions").MustArray()
-					cond := simplejson.NewFromAny(conditions[0])
+				Convey("should be able to read interval", func() {
+					So(len(alerts), ShouldEqual, 1)
 
-					So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
-				}
+					for _, alert := range alerts {
+						So(alert.DashboardId, ShouldEqual, 4)
+
+						conditions := alert.Settings.Get("conditions").MustArray()
+						cond := simplejson.NewFromAny(conditions[0])
+
+						So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
+					}
+				})
 			})
-		})
 
-		Convey("Should be able to extract collapsed panels", func() {
-			json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
-			So(err, ShouldBeNil)
+			Convey("Should be able to extract collapsed panels", func() {
+				json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
+				So(err, ShouldBeNil)
 
-			dashJson, err := simplejson.NewJson(json)
-			So(err, ShouldBeNil)
+				dashJson, err := simplejson.NewJson(json)
+				So(err, ShouldBeNil)
 
-			dash := m.NewDashboardFromJson(dashJson)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
+				dash := m.NewDashboardFromJson(dashJson)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
-			alerts, err := extractor.GetAlerts()
+				alerts, err := extractor.GetAlerts()
 
-			Convey("Get rules without error", func() {
-				So(err, ShouldBeNil)
-			})
+				Convey("Get rules without error", func() {
+					So(err, ShouldBeNil)
+				})
 
-			Convey("should be able to extract collapsed alerts", func() {
-				So(len(alerts), ShouldEqual, 4)
+				Convey("should be able to extract collapsed alerts", func() {
+					So(len(alerts), ShouldEqual, 4)
+				})
 			})
-		})
 
-		Convey("Parse and validate dashboard without id and containing an alert", func() {
-			json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
-			So(err, ShouldBeNil)
+			Convey("Parse and validate dashboard without id and containing an alert", func() {
+				json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
+				So(err, ShouldBeNil)
 
-			dashJSON, err := simplejson.NewJson(json)
-			So(err, ShouldBeNil)
-			dash := m.NewDashboardFromJson(dashJSON)
-			extractor := NewDashAlertExtractor(dash, 1, nil)
+				dashJSON, err := simplejson.NewJson(json)
+				So(err, ShouldBeNil)
+				dash := m.NewDashboardFromJson(dashJSON)
+				extractor := NewDashAlertExtractor(dash, 1, nil)
 
-			err = extractor.ValidateAlerts()
+				err = extractor.ValidateAlerts()
 
-			Convey("Should validate without error", func() {
-				So(err, ShouldBeNil)
-			})
+				Convey("Should validate without error", func() {
+					So(err, ShouldBeNil)
+				})
 
-			Convey("Should fail on save", func() {
-				_, err := extractor.GetAlerts()
-				So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
+				Convey("Should fail on save", func() {
+					_, err := extractor.GetAlerts()
+					So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
+				})
 			})
 		})
 	})

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

@@ -24,7 +24,7 @@ type Notifier interface {
 	// ShouldNotify checks this evaluation should send an alert notification
 	ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
 
-	GetNotifierId() int64
+	GetNotifierUid() string
 	GetIsDefault() bool
 	GetSendReminder() bool
 	GetDisableResolveMessage() bool

+ 6 - 6
pkg/services/alerting/notifier.go

@@ -60,13 +60,13 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error {
 func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
 	notifier := notifierState.notifier
 
-	n.log.Debug("Sending notification", "type", notifier.GetType(), "id", notifier.GetNotifierId(), "isDefault", notifier.GetIsDefault())
+	n.log.Debug("Sending notification", "type", notifier.GetType(), "uid", notifier.GetNotifierUid(), "isDefault", notifier.GetIsDefault())
 	metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
 
 	err := notifier.Notify(evalContext)
 
 	if err != nil {
-		n.log.Error("failed to send notification", "id", notifier.GetNotifierId(), "error", err)
+		n.log.Error("failed to send notification", "uid", notifier.GetNotifierUid(), "error", err)
 	}
 
 	if evalContext.IsTestRun {
@@ -110,7 +110,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi
 	for _, notifierState := range notifierStates {
 		err := n.sendNotification(evalContext, notifierState)
 		if err != nil {
-			n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId(), "error", err)
+			n.log.Error("failed to send notification", "uid", notifierState.notifier.GetNotifierUid(), "error", err)
 		}
 	}
 
@@ -157,8 +157,8 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	return nil
 }
 
-func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) {
-	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
+func (n *notificationService) getNeededNotifiers(orgId int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) {
+	query := &m.GetAlertNotificationsWithUidToSendQuery{OrgId: orgId, Uids: notificationUids}
 
 	if err := bus.Dispatch(query); err != nil {
 		return nil, err
@@ -168,7 +168,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
 	for _, notification := range query.Result {
 		not, err := InitNotifier(notification)
 		if err != nil {
-			n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
+			n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err)
 			continue
 		}
 

+ 4 - 4
pkg/services/alerting/notifiers/base.go

@@ -16,7 +16,7 @@ const (
 type NotifierBase struct {
 	Name                  string
 	Type                  string
-	Id                    int64
+	Uid                   string
 	IsDeault              bool
 	UploadImage           bool
 	SendReminder          bool
@@ -34,7 +34,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
 	}
 
 	return NotifierBase{
-		Id:                    model.Id,
+		Uid:                   model.Uid,
 		Name:                  model.Name,
 		IsDeault:              model.IsDefault,
 		Type:                  model.Type,
@@ -110,8 +110,8 @@ func (n *NotifierBase) NeedsImage() bool {
 	return n.UploadImage
 }
 
-func (n *NotifierBase) GetNotifierId() int64 {
-	return n.Id
+func (n *NotifierBase) GetNotifierUid() string {
+	return n.Uid
 }
 
 func (n *NotifierBase) GetIsDefault() bool {

+ 1 - 1
pkg/services/alerting/notifiers/base_test.go

@@ -173,7 +173,7 @@ func TestBaseNotifier(t *testing.T) {
 		bJson := simplejson.New()
 
 		model := &m.AlertNotification{
-			Id:       1,
+			Uid:      "1",
 			Name:     "name",
 			Type:     "email",
 			Settings: bJson,

+ 9 - 5
pkg/services/alerting/rule.go

@@ -30,7 +30,7 @@ type Rule struct {
 	ExecutionErrorState m.ExecutionErrorOption
 	State               m.AlertStateType
 	Conditions          []Condition
-	Notifications       []int64
+	Notifications       []string
 
 	StateChanges int64
 }
@@ -126,11 +126,15 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
 
 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
 		jsonModel := simplejson.NewFromAny(v)
-		id, err := jsonModel.Get("id").Int64()
-		if err != nil {
-			return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+		if id, err := jsonModel.Get("id").Int64(); err == nil {
+			model.Notifications = append(model.Notifications, fmt.Sprintf("%09d", id))
+		} else {
+			if uid, err := jsonModel.Get("uid").String(); err != nil {
+				return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+			} else {
+				model.Notifications = append(model.Notifications, uid)
+			}
 		}
-		model.Notifications = append(model.Notifications, id)
 	}
 
 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {

部分文件因为文件数量过多而无法显示