Browse Source

Merge branch 'master' of https://github.com/grafana/grafana into piechart-react

corpglory-dev 6 years ago
parent
commit
a550e5388a
100 changed files with 3919 additions and 541 deletions
  1. 0 16
      .github/ISSUE_TEMPLATE.md
  2. 27 0
      .github/ISSUE_TEMPLATE/1-bug_report.md
  3. 11 0
      .github/ISSUE_TEMPLATE/2-feature_request.md
  4. 26 0
      .github/ISSUE_TEMPLATE/3-accessibility.md
  5. 14 0
      .github/ISSUE_TEMPLATE/4-question.md
  6. 26 4
      .github/PULL_REQUEST_TEMPLATE.md
  7. 1 0
      .prettierignore
  8. 19 3
      CHANGELOG.md
  9. 4 4
      CONTRIBUTING.md
  10. 276 36
      Gopkg.lock
  11. 6 2
      Gopkg.toml
  12. 0 6
      README.md
  13. 1 0
      conf/defaults.ini
  14. 1 0
      conf/sample.ini
  15. 296 0
      devenv/dev-dashboards/panel_tests_multiseries_gauge.json
  16. 1 1
      devenv/docker/blocks/prometheus2/Dockerfile
  17. 5 1
      docs/sources/alerting/notifications.md
  18. 4 4
      docs/sources/auth/generic-oauth.md
  19. 1 0
      docs/sources/features/datasources/prometheus.md
  20. 11 3
      docs/sources/installation/configuration.md
  21. 2 2
      docs/sources/installation/upgrading.md
  22. 2 2
      latest.json
  23. 10 7
      package.json
  24. 30 0
      packages/grafana-ui/README.md
  25. 2 0
      packages/grafana-ui/package.json
  26. 54 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx
  27. 64 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx
  28. 239 0
      packages/grafana-ui/src/components/BarGauge/BarGauge.tsx
  29. 9 0
      packages/grafana-ui/src/components/BarGauge/_BarGauge.scss
  30. 358 0
      packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap
  31. 3 3
      packages/grafana-ui/src/components/ColorPicker/ColorInput.tsx
  32. 10 1
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx
  33. 23 0
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.test.tsx
  34. 31 21
      packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx
  35. 5 5
      packages/grafana-ui/src/components/ColorPicker/ColorPickerPopover.tsx
  36. 56 0
      packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx
  37. 2 2
      packages/grafana-ui/src/components/ColorPicker/NamedColorsPalette.story.tsx
  38. 0 53
      packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss
  39. 2 2
      packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts
  40. 15 0
      packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx
  41. 11 18
      packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx
  42. 1 1
      packages/grafana-ui/src/components/FormField/_FormField.scss
  43. 22 37
      packages/grafana-ui/src/components/Gauge/Gauge.tsx
  44. 1 1
      packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss
  45. 1 1
      packages/grafana-ui/src/components/Select/_Select.scss
  46. 99 0
      packages/grafana-ui/src/components/Table/Table.story.tsx
  47. 287 0
      packages/grafana-ui/src/components/Table/Table.tsx
  48. 291 0
      packages/grafana-ui/src/components/Table/TableCellBuilder.tsx
  49. 25 0
      packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx
  50. 22 0
      packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx
  51. 95 0
      packages/grafana-ui/src/components/Table/TableInputCSV.tsx
  52. 80 0
      packages/grafana-ui/src/components/Table/_Table.scss
  53. 24 0
      packages/grafana-ui/src/components/Table/_TableInputCSV.scss
  54. 167 0
      packages/grafana-ui/src/components/Table/examples.ts
  55. 2 3
      packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss
  56. 77 0
      packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx
  57. 3 0
      packages/grafana-ui/src/components/index.scss
  58. 6 2
      packages/grafana-ui/src/components/index.ts
  59. 23 23
      packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts
  60. 23 23
      packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts
  61. 43 72
      packages/grafana-ui/src/themes/_variables.scss.tmpl.ts
  62. 14 10
      packages/grafana-ui/src/themes/dark.ts
  63. 35 25
      packages/grafana-ui/src/themes/default.ts
  64. 16 12
      packages/grafana-ui/src/themes/light.ts
  65. 11 5
      packages/grafana-ui/src/types/data.ts
  66. 3 1
      packages/grafana-ui/src/types/datasource.ts
  67. 1 0
      packages/grafana-ui/src/types/index.ts
  68. 15 12
      packages/grafana-ui/src/types/panel.ts
  69. 46 28
      packages/grafana-ui/src/types/theme.ts
  70. 5 0
      packages/grafana-ui/src/types/threshold.ts
  71. 66 0
      packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap
  72. 6 0
      packages/grafana-ui/src/utils/deprecationWarning.ts
  73. 4 0
      packages/grafana-ui/src/utils/index.ts
  74. 20 0
      packages/grafana-ui/src/utils/processTableData.test.ts
  75. 157 0
      packages/grafana-ui/src/utils/processTableData.ts
  76. 0 6
      packages/grafana-ui/src/utils/propDeprecationWarning.ts
  77. 33 0
      packages/grafana-ui/src/utils/singlestat.ts
  78. 24 0
      packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx
  79. 15 0
      packages/grafana-ui/src/utils/string.test.ts
  80. 13 0
      packages/grafana-ui/src/utils/string.ts
  81. 23 0
      packages/grafana-ui/src/utils/thresholds.ts
  82. 1 1
      packages/grafana-ui/src/utils/valueFormats/categories.ts
  83. 2 1
      packaging/docker/Dockerfile
  84. 1 0
      pkg/api/dashboard.go
  85. 124 0
      pkg/api/dashboard_test.go
  86. 7 5
      pkg/api/frontendsettings.go
  87. 1 0
      pkg/api/login.go
  88. 9 2
      pkg/log/log.go
  89. 23 2
      pkg/login/ldap.go
  90. 144 3
      pkg/login/ldap_test.go
  91. 1 1
      pkg/services/alerting/notifier.go
  92. 68 19
      pkg/services/alerting/notifiers/dingding.go
  93. 43 38
      pkg/services/alerting/notifiers/discord.go
  94. 1 1
      pkg/services/alerting/test_notification.go
  95. 15 4
      pkg/services/rendering/phantomjs.go
  96. 3 1
      pkg/services/sqlstore/alert.go
  97. 6 2
      pkg/services/sqlstore/annotation.go
  98. 3 2
      pkg/services/sqlstore/dashboard_version.go
  99. 6 1
      pkg/services/sqlstore/datasource.go
  100. 4 0
      pkg/services/sqlstore/login_attempt.go

+ 0 - 16
.github/ISSUE_TEMPLATE.md

@@ -1,16 +0,0 @@
-Read before posting: 
-
-- Questions should be posted to https://community.grafana.com. Please search there and here on GitHub for similar issues before creating a new issue. 
-- Checkout FAQ: https://community.grafana.com/c/howto/faq
-- Checkout How to troubleshoot metric query issues: https://community.grafana.com/t/how-to-troubleshoot-metric-query-issues/50
-
-Please include this information:
-### What Grafana version are you using?
-### What datasource are you using?
-### What OS are you running grafana on?
-### What did you do?
-### What was the expected result?
-### What happened instead?
-### If related to metric query / data viz:
-### Include raw network request & response: get by opening Chrome Dev Tools (F12, Ctrl+Shift+I on windows, Cmd+Opt+I on Mac), go the network tab.
- 

+ 27 - 0
.github/ISSUE_TEMPLATE/1-bug_report.md

@@ -0,0 +1,27 @@
+---
+name: Bug report
+about: Report a bug you found when using Grafana
+labels: 'type: bug'
+---
+
+<!--
+Please use this template while reporting a bug and provide as much info as possible.
+Questions should be posted to https://community.grafana.com
+Use query inspector to troubleshoot issues: https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630
+-->
+
+**What happened**:
+
+**What you expected to happen**:
+
+**How to reproduce it (as minimally and precisely as possible)**:
+
+**Anything else we need to know?**:
+
+**Environment**:
+- Grafana version:
+- Data source type & version:
+- OS Grafana is installed on:
+- User OS & Browser:
+- Grafana plugins:
+- Others:

+ 11 - 0
.github/ISSUE_TEMPLATE/2-feature_request.md

@@ -0,0 +1,11 @@
+---
+name: Enhancement request
+about: Suggest an enhancement or new feature for the Grafana project
+labels: 'type: feature request'
+---
+
+<!-- Please only use this template for submitting feature requests -->
+
+**What would you like to be added**:
+
+**Why is this needed**:

+ 26 - 0
.github/ISSUE_TEMPLATE/3-accessibility.md

@@ -0,0 +1,26 @@
+---
+name: Accessibility issue
+about: Help make Grafana be better at keyboard navigation, screen-readable and accessible to all.
+labels: 'type: accessibility'
+---
+
+<!--
+Please only use this template for submitting accessibility issues.
+
+This is a new feature area for Grafana that we want to improve. We have long way to go
+to really improve accessibility and would like your help to know where to start.
+-->
+
+**Steps to reproduce**:
+
+**Actual Result**:
+
+**Expected Result**
+
+**Relevant WCAG Criteria:** [#.#.# WCAG Criterion](link to https://www.w3.org/WAI/WCAG21/quickref/?versions=2.0)
+
+**Environment**:
+- Grafana version:
+- Data source type & version:
+- User OS & Browser:
+- Others:

+ 14 - 0
.github/ISSUE_TEMPLATE/4-question.md

@@ -0,0 +1,14 @@
+---
+name: Support request
+about: 'Question or support request relating to using Grafana'
+title: ''
+labels: ''
+assignees: ''
+---
+
+STOP -- PLEASE READ!
+
+GitHub is not the right place for questions and support requests.
+
+Please ask questions on our community site: [https://community.grafana.com/](https://community.grafana.com/)
+

+ 26 - 4
.github/PULL_REQUEST_TEMPLATE.md

@@ -1,5 +1,27 @@
-* Follow the contribution guidelines in [`CONTRIBUTING.md`](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md)
-* Rebase your PR if it gets out of sync with master
-* Include `closes #<issue>` or a link to the issue in the description
+<!--  Thanks for sending a pull request!  Here are some tips for you:
 
-**REMOVE THE TEXT ABOVE BEFORE CREATING THE PULL REQUEST**
+1. If this is your first time, please read our [`CONTRIBUTING.md`](https://github.com/grafana/grafana/blob/master/CONTRIBUTING.md) guide.
+2. Ensure you have added or ran the appropriate tests for your PR.
+3. If it's a new feature or config option it will need a docs update. Docs are under the docs folder in repo root.
+4. If the PR is unfinished, mark it as a draft PR.
+5. Rebase your PR if it gets out of sync with master
+-->
+
+**What this PR does / why we need it**:
+
+**Which issue(s) this PR fixes**:
+<!--
+*Automatically closes linked issue when PR is merged.
+Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
+-->
+Fixes #
+
+**Special notes for your reviewer**:
+
+**Release note**:
+<!--
+If this is a user facing change and should be mentioned in release note add it below. If no, just write "NONE" below.
+-->
+```release-note
+
+```

+ 1 - 0
.prettierignore

@@ -5,4 +5,5 @@ pkg/
 node_modules
 public/vendor/
 vendor/
+data/
 

+ 19 - 3
CHANGELOG.md

@@ -1,26 +1,42 @@
 # 6.1.0 (unreleased)
 
+### New Features
+* **Prometheus**: adhoc filter support [#8253](https://github.com/grafana/grafana/issues/8253), thx [@mtanda](https://github.com/mtanda)
+
 ### Minor
 * **Cloudwatch**: Add AWS RDS MaximumUsedTransactionIDs metric [#15077](https://github.com/grafana/grafana/pull/15077), thx [@activeshadow](https://github.com/activeshadow)
+* **Heatmap**: `Middle` bucket bound option [#15683](https://github.com/grafana/grafana/issues/15683)
+* **Heatmap**: `Reverse order` option for changing order of buckets [#15683](https://github.com/grafana/grafana/issues/15683)
 
 ### Bug Fixes
 * **Api**: Invalid org invite code [#10506](https://github.com/grafana/grafana/issues/10506)
 * **Datasource**: Handles nil jsondata field gracefully [#14239](https://github.com/grafana/grafana/issues/14239)
 * **Gauge**: Interpolate scoped variables in repeated gauges [#15739](https://github.com/grafana/grafana/issues/15739)
 * **Datasource**: Empty user/password was not updated when updating datasources [#15608](https://github.com/grafana/grafana/pull/15608), thx [@Maddin-619](https://github.com/Maddin-619)
+* **Heatmap**: legend shows wrong colors for small values [#14019](https://github.com/grafana/grafana/issues/14019)
 
-# 6.0.1 (unreleased)
+# 6.0.1 (2019-03-06)
 
 ### Bug Fixes
 * **Metrics**: Fixes broken usagestats metrics for /metrics [#15651](https://github.com/grafana/grafana/issues/15651)
 * **Dashboard**: Fixes kiosk mode should have &kiosk appended to the url [#15765](https://github.com/grafana/grafana/issues/15765)
 * **Dashboard**: Fixes kiosk=tv mode with autofitpanels should respect header [#15650](https://github.com/grafana/grafana/issues/15650)
+* **Image rendering**: Fixed image rendering issue for dashboards with auto refresh, . [#15818](https://github.com/grafana/grafana/pull/15818), [@torkelo](https://github.com/torkelo)
+* **Dashboard**: Fix only users that can edit a dashboard should be able to update panel json. [#15805](https://github.com/grafana/grafana/pull/15805), [@marefr](https://github.com/marefr)
+* **LDAP**: fix allow anonymous initial bind for ldap search. [#15803](https://github.com/grafana/grafana/pull/15803), [@marefr](https://github.com/marefr)
+* **UX**: Fixed scrollbar not visible initially (only after manual scroll). [#15798](https://github.com/grafana/grafana/pull/15798), [@torkelo](https://github.com/torkelo)
+* **Datasource admin** TestData   [#15793](https://github.com/grafana/grafana/pull/15793), [@hugohaggmark](https://github.com/hugohaggmark)
+* **Dashboard**: Fixed scrolling issue that caused scroll to be locked to bottom. [#15792](https://github.com/grafana/grafana/pull/15792), [@torkelo](https://github.com/torkelo)
+* **Explore**: Viewers with viewers_can_edit should be able to access /explore. [#15787](https://github.com/grafana/grafana/pull/15787), [@jschill](https://github.com/jschill)
+* **Security** fix: limit access to org admin and alerting pages. [#15761](https://github.com/grafana/grafana/pull/15761), [@marefr](https://github.com/marefr)
+* **Panel Edit** minInterval changes did not persist [#15757](https://github.com/grafana/grafana/pull/15757), [@hugohaggmark](https://github.com/hugohaggmark)
+* **Teams**: Fixed bug when getting teams for user. [#15595](https://github.com/grafana/grafana/pull/15595), [@hugohaggmark](https://github.com/hugohaggmark)
+* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509)
+* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179)
 
 # 6.0.0 stable (2019-02-25)
 
 ### Bug Fixes
-* **Stackdriver**: fix for float64 bounds for distribution metrics [#14509](https://github.com/grafana/grafana/issues/14509)
-* **Stackdriver**: no reducers available for distribution type [#15179](https://github.com/grafana/grafana/issues/15179)
 * **Dashboard**: fixes click after scroll in series override menu [#15621](https://github.com/grafana/grafana/issues/15621)
 * **MySQL**: fix mysql query using _interval_ms variable throws error [#14507](https://github.com/grafana/grafana/issues/14507)
 

+ 4 - 4
CONTRIBUTING.md

@@ -34,10 +34,10 @@ To setup a local development environment we recommend reading [Building Grafana
 ### Pull requests with new features
 Commits should be as small as possible, while ensuring that each commit is correct independently (i.e., each commit should compile and pass tests).
 
-Make sure to include `closes #<issue>` or `fixes #<issue>` in the pull request description.
+Make sure to include `Closes #<issue number>` or `Fixes #<issue number>` in the pull request description.
 
 ### Pull requests with bug fixes
-Please make all changes in one commit if possible. Include `closes #12345` in bottom of the commit message.
+Please make all changes in one commit if possible. Include `Closes #<issue number>` in bottom of the commit message.
 A commit message for a bug fix should look something like this.
 
 ```
@@ -48,7 +48,7 @@ provsioners each provisioner overwrite each other.
 filling up dashboard_versions quite fast if using
 default settings.
 
-closes #12864
+Closes #12864
 ```
 
-If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged.
+If the pull request needs changes before its merged the new commits should be rebased into one commit before its merged.

+ 276 - 36
Gopkg.lock

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

+ 6 - 2
Gopkg.toml

@@ -81,11 +81,15 @@ ignored = [
 
 [[constraint]]
   name = "github.com/go-xorm/core"
-  version = "=0.5.7"
+  version = "=0.6.2"
+
+[[override]]
+  name = "github.com/go-xorm/builder"
+  version = "=0.3.4"
 
 [[constraint]]
   name = "github.com/go-xorm/xorm"
-  version = "=0.6.4"
+  version = "=0.7.1"
 
 [[constraint]]
   name = "github.com/gorilla/websocket"

+ 0 - 6
README.md

@@ -7,12 +7,6 @@
 Grafana is an open source, feature rich metrics dashboard and graph editor for
 Graphite, Elasticsearch, OpenTSDB, Prometheus and InfluxDB.
 
-![](https://www.grafanacon.org/2019/images/grafanacon_la_nav-logo.png)
-
-Join us Feb 25-26 in Los Angeles, California for GrafanaCon - a two-day event with talks focused on Grafana and the surrounding open source monitoring ecosystem. Get deep dives into Loki, the Explore workflow and all of the new features of Grafana 6, plus participate in hands on workshops to help you get the most out of your data.
-
-Time is running out - grab your ticket now! http://grafanacon.org
-
 <!---
 ![](http://docs.grafana.org/assets/img/features/dashboard_ex1.png)
 -->

+ 1 - 0
conf/defaults.ini

@@ -231,6 +231,7 @@ verify_email_enabled = false
 
 # Background text for the user field on the login page
 login_hint = email or username
+password_hint = password
 
 # Default UI theme ("dark" or "light")
 default_theme = dark

+ 1 - 0
conf/sample.ini

@@ -211,6 +211,7 @@ log_queries =
 
 # Background text for the user field on the login page
 ;login_hint = email or username
+;password_hint = password
 
 # Default UI theme ("dark" or "light")
 ;default_theme = dark

+ 296 - 0
devenv/dev-dashboards/panel_tests_multiseries_gauge.json

@@ -0,0 +1,296 @@
+{
+  "annotations": {
+    "list": [
+      {
+        "builtIn": 1,
+        "datasource": "-- Grafana --",
+        "enable": true,
+        "hide": true,
+        "iconColor": "rgba(0, 211, 255, 1)",
+        "name": "Annotations & Alerts",
+        "type": "dashboard"
+      }
+    ]
+  },
+  "editable": true,
+  "gnetId": null,
+  "graphTooltip": 0,
+  "links": [],
+  "panels": [
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 0
+      },
+      "id": 6,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#1F78C1",
+            "index": 5,
+            "value": 96.875
+          },
+          {
+            "color": "#E24D42",
+            "index": 4,
+            "value": 93.75
+          },
+          {
+            "color": "#EF843C",
+            "index": 3,
+            "value": 87.5
+          },
+          {
+            "color": "#6ED0E0",
+            "index": 2,
+            "value": 75
+          },
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": [
+          {
+            "from": "50",
+            "id": 1,
+            "operator": "",
+            "text": "Hello :) ",
+            "to": "90",
+            "type": 2,
+            "value": ""
+          }
+        ]
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Horizontal with range variable",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 8,
+        "w": 24,
+        "x": 0,
+        "y": 8
+      },
+      "id": 2,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": 100,
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "avg",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Repeat horizontal",
+      "type": "gauge"
+    },
+    {
+      "datasource": "gdev-testdata",
+      "gridPos": {
+        "h": 14,
+        "w": 5,
+        "x": 0,
+        "y": 16
+      },
+      "id": 4,
+      "links": [],
+      "options-gauge": {
+        "decimals": 0,
+        "maxValue": "200",
+        "minValue": 0,
+        "options": {
+          "decimals": 0,
+          "maxValue": 100,
+          "minValue": 0,
+          "prefix": "",
+          "showThresholdLabels": false,
+          "showThresholdMarkers": true,
+          "stat": "avg",
+          "suffix": "",
+          "thresholds": [],
+          "unit": "none",
+          "valueMappings": []
+        },
+        "prefix": "",
+        "showThresholdLabels": false,
+        "showThresholdMarkers": true,
+        "stat": "max",
+        "suffix": "",
+        "thresholds": [
+          {
+            "color": "#6ED0E0",
+            "index": 2,
+            "value": 75
+          },
+          {
+            "color": "#EAB839",
+            "index": 1,
+            "value": 50
+          },
+          {
+            "color": "#7EB26D",
+            "index": 0,
+            "value": null
+          }
+        ],
+        "unit": "none",
+        "valueMappings": []
+      },
+      "targets": [
+        {
+          "refId": "A",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "B",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "C",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "D",
+          "scenarioId": "random_walk"
+        },
+        {
+          "refId": "E",
+          "scenarioId": "random_walk"
+        }
+      ],
+      "timeFrom": null,
+      "timeShift": null,
+      "title": "Vertical",
+      "type": "gauge"
+    }
+  ],
+  "schemaVersion": 17,
+  "style": "dark",
+  "tags": [],
+  "templating": {
+    "list": []
+  },
+  "time": {
+    "from": "now-6h",
+    "to": "now"
+  },
+  "timepicker": {
+    "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
+    "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+  },
+  "timezone": "",
+  "title": "Multi series gauges",
+  "uid": "szkuR1umk",
+  "version": 7
+}

+ 1 - 1
devenv/docker/blocks/prometheus2/Dockerfile

@@ -1,3 +1,3 @@
-FROM prom/prometheus:v2.2.0
+FROM prom/prometheus:v2.7.2
 ADD prometheus.yml /etc/prometheus/
 ADD alert.rules /etc/prometheus/

+ 5 - 1
docs/sources/alerting/notifications.md

@@ -83,7 +83,11 @@ or a bot integration via Slack Apps. Follow Slack's guide to set up a bot integr
 
 Setting | Description
 ---------- | -----------
-Recipient | allows you to override the Slack recipient.
+Url | Slack incoming webhook url.
+Username | Set the username for the bot's message.
+Recipient | Allows you to override the Slack recipient.
+Icon emoji | Provide an emoji to use as the icon for the bot's message. Ex :smile:
+Icon URL | Provide a url to an image to use as the icon for the bot's message.
 Mention | make it possible to include a mention in the Slack notification sent by Grafana. Ex @here or @channel
 Token | If provided, Grafana will upload the generated image via Slack's file.upload API method, not the external image destination.
 

+ 4 - 4
docs/sources/auth/generic-oauth.md

@@ -217,10 +217,10 @@ Some OAuth2 providers might not support `client_id` and `client_secret` passed v
 results in `invalid_client` error. To allow Grafana to authenticate via these type of providers, the client identifiers must be
 send via POST body, which can be enabled via the following settings:
 
-    ```bash
-    [auth.generic_oauth]
-    send_client_credentials_via_post = true
-    ```
+```bash
+[auth.generic_oauth]
+send_client_credentials_via_post = true
+```
 
 <hr>
 

+ 1 - 0
docs/sources/features/datasources/prometheus.md

@@ -68,6 +68,7 @@ provides the following functions you can use in the `Query` input field.
 
 Name | Description
 ---- | --------
+*label_names()* | Returns a list of label names.
 *label_values(label)* | Returns a list of label values for the `label` in every metric.
 *label_values(metric, label)* | Returns a list of label values for the `label` in the specified metric.
 *metrics(metric)* | Returns a list of metrics matching the specified `metric` regex.

+ 11 - 3
docs/sources/installation/configuration.md

@@ -162,9 +162,9 @@ executed with working directory set to the installation path.
 
 ### enable_gzip
 
-Set this option to `true` to enable HTTP compression, this can improve 
-transfer speed and bandwidth utilization. It is recommended that most 
-users set it to `true`. By default it is set to `false` for compatibility 
+Set this option to `true` to enable HTTP compression, this can improve
+transfer speed and bandwidth utilization. It is recommended that most
+users set it to `true`. By default it is set to `false` for compatibility
 reasons.
 
 ### cert_file
@@ -342,6 +342,14 @@ options are `Admin` and `Editor`. e.g. :
 Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
 Defaults to `false`.
 
+### login_hint
+
+Text used as placeholder text on login page for login/username input.
+
+### password_hint
+
+Text used as placeholder text on login page for password input.
+
 <hr>
 
 ## [auth]

+ 2 - 2
docs/sources/installation/upgrading.md

@@ -120,7 +120,7 @@ If you're using systemd and have a large amount of annotations consider temporar
 
 ## Upgrading to v6.0
 
-If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitzied HTML.
+If you have text panels with script tags they will no longer work due to a new setting that per default disallow unsanitized HTML.
 Read more [here](/installation/configuration/#disable-sanitize-html) about this new setting.
 
 ### Authentication and security
@@ -147,4 +147,4 @@ login_maximum_inactive_lifetime_days = 1
 login_maximum_lifetime_days = 1
 ```
 
-The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings.
+The default cookie name for storing the auth token is `grafana_session`. you can configure this with `login_cookie_name` in `[auth]` settings.

+ 2 - 2
latest.json

@@ -1,4 +1,4 @@
 {
-  "stable": "6.0.0",
-  "testing": "6.0.0"
+  "stable": "6.0.1",
+  "testing": "6.0.1"
 }

+ 10 - 7
package.json

@@ -17,6 +17,7 @@
     "@babel/preset-react": "^7.0.0",
     "@babel/preset-typescript": "^7.1.0",
     "@rtsao/plugin-proposal-class-properties": "^7.0.1-patch.1",
+    "@types/angular": "^1.6.6",
     "@types/chalk": "^2.2.0",
     "@types/classnames": "^2.2.6",
     "@types/commander": "^2.12.2",
@@ -123,10 +124,10 @@
   },
   "scripts": {
     "dev": "webpack --progress --colors --mode development --config scripts/webpack/webpack.dev.js",
-    "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme",
-    "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot --theme",
-    "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --hot",
-    "watch": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --theme -d watch,start",
+    "start": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --watchTheme",
+    "start:hot": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot --watchTheme",
+    "start:ignoreTheme": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts core:start --hot",
+    "watch": "yarn start -d watch,start core:start --watchTheme ",
     "build": "grunt build",
     "test": "grunt test",
     "tslint": "tslint -c tslint.json --project tsconfig.json",
@@ -136,8 +137,11 @@
     "storybook": "cd packages/grafana-ui && yarn storybook",
     "themes:generate": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/generateSassVariableFiles.ts",
     "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"",
-    "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --build",
-    "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts --release"
+    "gui:build": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:build",
+    "gui:releasePrepare": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release",
+    "gui:publish": "cd packages/grafana-ui/dist && npm publish --access public",
+    "gui:release": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts gui:release -p --createVersionCommit",
+    "cli": "ts-node --project ./scripts/cli/tsconfig.json ./scripts/cli/index.ts"
   },
   "husky": {
     "hooks": {
@@ -169,7 +173,6 @@
     "angular-native-dragdrop": "1.2.2",
     "angular-route": "1.6.6",
     "angular-sanitize": "1.6.6",
-    "ansicolor": "1.1.78",
     "baron": "^3.0.3",
     "brace": "^0.10.0",
     "classnames": "^2.2.6",

+ 30 - 0
packages/grafana-ui/README.md

@@ -12,6 +12,36 @@ See [package source](https://github.com/grafana/grafana/tree/master/packages/gra
 
 `npm install @grafana/ui`
 
+## Development
+
+For development purposes we suggest using `yarn link` that will create symlink to @grafana/ui lib. To do so navigate to `packages/grafana-ui` and run `yarn link`. Then, navigate to your project and run `yarn link @grafana/ui` to use the linked version of the lib. To unlink follow the same procedure, but use `yarn unlink` instead.
+
+## Building @grafana/ui
+To build @grafana/ui run `npm run gui:build` script *from Grafana repository root*. The build will be created in `packages/grafana-ui/dist` directory. Following steps from [Development](#development) you can test built package.
+
+## Releasing new version
+To release new version run `npm run gui:release` script *from Grafana repository root*. The script will prepare the distribution package as well as prompt you to bump library version and publish it to the NPM registry.
+
+### Automatic version bump
+When running `npm run gui:release` package.json file will be automatically updated. Also, package.json file will be commited and pushed to upstream branch.
+
+### Manual version bump
+To use `package.json` defined version run `npm run gui:release --usePackageJsonVersion` *from Grafana repository root*.
+
+### Preparing release package without publishing to NPM registry
+For testing purposes there is `npm run gui:releasePrepare` task that prepares distribution package without publishing it to the NPM registry.
+
+### V1 release process overview
+1. Package is compiled with TSC. Typings are created in `/dist` directory, and the compiled js lands in `/compiled` dir
+2. Rollup creates a CommonJS package based on compiled sources, and outputs it to `/dist` directory
+3. Readme, changelog and index.js files are moved to `/dist` directory
+4. Package version is bumped in both `@grafana/ui` package dir and in dist directory.
+5. Version commit is created and pushed to master branch
+5. Package is published to npm
+
+
 ## Versioning
 To limit the confusion related to @grafana/ui and Grafana versioning we decided to keep the major version in sync between those two.
 This means, that first version of @grafana/ui is taged with 6.0.0-alpha.0 to keep version in sync with Grafana 6.0 release.
+
+

+ 2 - 0
packages/grafana-ui/package.json

@@ -25,6 +25,7 @@
     "jquery": "^3.2.1",
     "lodash": "^4.17.10",
     "moment": "^2.22.2",
+    "papaparse": "^4.6.3",
     "react": "^16.6.3",
     "react-color": "^2.17.0",
     "react-custom-scrollbars": "^4.2.1",
@@ -48,6 +49,7 @@
     "@types/jquery": "^1.10.35",
     "@types/lodash": "^4.14.119",
     "@types/node": "^10.12.18",
+    "@types/papaparse": "^4.5.9",
     "@types/react": "^16.7.6",
     "@types/react-custom-scrollbars": "^4.0.5",
     "@types/react-test-renderer": "^16.0.3",

+ 54 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.story.tsx

@@ -0,0 +1,54 @@
+import { storiesOf } from '@storybook/react';
+import { number, text } from '@storybook/addon-knobs';
+import { BarGauge } from './BarGauge';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
+
+const getKnobs = () => {
+  return {
+    value: number('value', 70),
+    minValue: number('minValue', 0),
+    maxValue: number('maxValue', 100),
+    threshold1Value: number('threshold1Value', 40),
+    threshold1Color: text('threshold1Color', 'orange'),
+    threshold2Value: number('threshold2Value', 60),
+    threshold2Color: text('threshold2Color', 'red'),
+    unit: text('unit', 'ms'),
+    decimals: number('decimals', 1),
+  };
+};
+
+const BarGaugeStories = storiesOf('UI/BarGauge/BarGauge', module);
+
+BarGaugeStories.addDecorator(withCenteredStory);
+
+BarGaugeStories.add('Vertical, with basic thresholds', () => {
+  const {
+    value,
+    minValue,
+    maxValue,
+    threshold1Color,
+    threshold2Color,
+    threshold1Value,
+    threshold2Value,
+    unit,
+    decimals,
+  } = getKnobs();
+
+  return renderComponentWithTheme(BarGauge, {
+    width: 200,
+    height: 400,
+    value: value,
+    minValue: minValue,
+    maxValue: maxValue,
+    unit: unit,
+    prefix: '',
+    postfix: '',
+    decimals: decimals,
+    thresholds: [
+      { index: 0, value: -Infinity, color: 'green' },
+      { index: 1, value: threshold1Value, color: threshold1Color },
+      { index: 1, value: threshold2Value, color: threshold2Color },
+    ],
+  });
+});

+ 64 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.test.tsx

@@ -0,0 +1,64 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+import { BarGauge, Props } from './BarGauge';
+import { VizOrientation } from '../../types';
+import { getTheme } from '../../themes';
+
+jest.mock('jquery', () => ({
+  plot: jest.fn(),
+}));
+
+const setup = (propOverrides?: object) => {
+  const props: Props = {
+    maxValue: 100,
+    valueMappings: [],
+    minValue: 0,
+    prefix: '',
+    suffix: '',
+    thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }],
+    unit: 'none',
+    height: 300,
+    width: 300,
+    value: 25,
+    decimals: 0,
+    theme: getTheme(),
+    orientation: VizOrientation.Horizontal,
+  };
+
+  Object.assign(props, propOverrides);
+
+  const wrapper = shallow(<BarGauge {...props} />);
+  const instance = wrapper.instance() as BarGauge;
+
+  return {
+    instance,
+    wrapper,
+  };
+};
+
+describe('Get font color', () => {
+  it('should get first threshold color when only one threshold', () => {
+    const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }] });
+
+    expect(instance.getValueColors().value).toEqual('#7EB26D');
+  });
+
+  it('should get the threshold color if value is same as a threshold', () => {
+    const { instance } = setup({
+      thresholds: [
+        { index: 2, value: 75, color: '#6ED0E0' },
+        { index: 1, value: 10, color: '#EAB839' },
+        { index: 0, value: -Infinity, color: '#7EB26D' },
+      ],
+    });
+
+    expect(instance.getValueColors().value).toEqual('#EAB839');
+  });
+});
+
+describe('Render BarGauge with basic options', () => {
+  it('should render', () => {
+    const { wrapper } = setup();
+    expect(wrapper).toMatchSnapshot();
+  });
+});

+ 239 - 0
packages/grafana-ui/src/components/BarGauge/BarGauge.tsx

@@ -0,0 +1,239 @@
+// Library
+import React, { PureComponent, CSSProperties } from 'react';
+import tinycolor from 'tinycolor2';
+
+// Utils
+import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+
+// Types
+import { Themeable, TimeSeriesValue, Threshold, ValueMapping, VizOrientation } from '../../types';
+
+const BAR_SIZE_RATIO = 0.8;
+
+export interface Props extends Themeable {
+  height: number;
+  unit: string;
+  width: number;
+  thresholds: Threshold[];
+  valueMappings: ValueMapping[];
+  value: TimeSeriesValue;
+  maxValue: number;
+  minValue: number;
+  orientation: VizOrientation;
+  prefix?: string;
+  suffix?: string;
+  decimals?: number;
+}
+
+/*
+ * This visualization is still in POC state, needed more tests & better structure
+ */
+export class BarGauge extends PureComponent<Props> {
+  static defaultProps: Partial<Props> = {
+    maxValue: 100,
+    minValue: 0,
+    value: 100,
+    unit: 'none',
+    orientation: VizOrientation.Horizontal,
+    thresholds: [],
+    valueMappings: [],
+  };
+
+  getNumericValue(): number {
+    if (Number.isFinite(this.props.value as number)) {
+      return this.props.value as number;
+    }
+    return 0;
+  }
+
+  getValueColors(): BarColors {
+    const { thresholds, theme, value } = this.props;
+
+    const activeThreshold = getThresholdForValue(thresholds, value);
+
+    if (activeThreshold !== null) {
+      const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
+
+      return {
+        value: color,
+        border: color,
+        bar: tinycolor(color)
+          .setAlpha(0.3)
+          .toRgbString(),
+      };
+    }
+
+    return {
+      value: getColorFromHexRgbOrName('gray', theme.type),
+      bar: getColorFromHexRgbOrName('gray', theme.type),
+      border: getColorFromHexRgbOrName('gray', theme.type),
+    };
+  }
+
+  getCellColor(positionValue: TimeSeriesValue): string {
+    const { thresholds, theme, value } = this.props;
+    const activeThreshold = getThresholdForValue(thresholds, positionValue);
+
+    if (activeThreshold !== null) {
+      const color = getColorFromHexRgbOrName(activeThreshold.color, theme.type);
+
+      // if we are past real value the cell is not "on"
+      if (value === null || (positionValue !== null && positionValue > value)) {
+        return tinycolor(color)
+          .setAlpha(0.15)
+          .toRgbString();
+      } else {
+        return tinycolor(color)
+          .setAlpha(0.7)
+          .toRgbString();
+      }
+    }
+
+    return 'gray';
+  }
+
+  getValueStyles(value: string, color: string, width: number): CSSProperties {
+    const guess = width / (value.length * 1.1);
+    const fontSize = Math.min(Math.max(guess, 14), 40);
+
+    return {
+      color: color,
+      fontSize: fontSize + 'px',
+    };
+  }
+
+  renderVerticalBar(valueFormatted: string, valuePercent: number) {
+    const { height, width } = this.props;
+
+    const maxHeight = height * BAR_SIZE_RATIO;
+    const barHeight = Math.max(valuePercent * maxHeight, 0);
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width);
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'column',
+      justifyContent: 'flex-end',
+    };
+
+    const barStyles: CSSProperties = {
+      height: `${barHeight}px`,
+      width: `${width}px`,
+      backgroundColor: colors.bar,
+      borderTop: `1px solid ${colors.border}`,
+    };
+
+    return (
+      <div style={containerStyles}>
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+        <div style={barStyles} />
+      </div>
+    );
+  }
+
+  renderHorizontalBar(valueFormatted: string, valuePercent: number) {
+    const { height, width } = this.props;
+
+    const maxWidth = width * BAR_SIZE_RATIO;
+    const barWidth = Math.max(valuePercent * maxWidth, 0);
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
+
+    valueStyles.marginLeft = '8px';
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    };
+
+    const barStyles = {
+      height: `${height}px`,
+      width: `${barWidth}px`,
+      backgroundColor: colors.bar,
+      borderRight: `1px solid ${colors.border}`,
+    };
+
+    return (
+      <div style={containerStyles}>
+        <div style={barStyles} />
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+      </div>
+    );
+  }
+
+  renderHorizontalLCD(valueFormatted: string, valuePercent: number) {
+    const { height, width, maxValue, minValue } = this.props;
+
+    const valueRange = maxValue - minValue;
+    const maxWidth = width * BAR_SIZE_RATIO;
+    const cellSpacing = 4;
+    const cellCount = 30;
+    const cellWidth = (maxWidth - cellSpacing * cellCount) / cellCount;
+    const colors = this.getValueColors();
+    const valueStyles = this.getValueStyles(valueFormatted, colors.value, width * (1 - BAR_SIZE_RATIO));
+    valueStyles.marginLeft = '8px';
+
+    const containerStyles: CSSProperties = {
+      width: `${width}px`,
+      height: `${height}px`,
+      display: 'flex',
+      flexDirection: 'row',
+      alignItems: 'center',
+    };
+
+    const cells: JSX.Element[] = [];
+
+    for (let i = 0; i < cellCount; i++) {
+      const currentValue = (valueRange / cellCount) * i;
+      const cellColor = this.getCellColor(currentValue);
+      const cellStyles: CSSProperties = {
+        width: `${cellWidth}px`,
+        backgroundColor: cellColor,
+        marginRight: '4px',
+        height: `${height}px`,
+        borderRadius: '2px',
+      };
+
+      cells.push(<div style={cellStyles} />);
+    }
+
+    return (
+      <div style={containerStyles}>
+        {cells}
+        <div className="bar-gauge__value" style={valueStyles}>
+          {valueFormatted}
+        </div>
+      </div>
+    );
+  }
+
+  render() {
+    const { maxValue, minValue, orientation, unit, decimals } = this.props;
+
+    const numericValue = this.getNumericValue();
+    const valuePercent = Math.min(numericValue / (maxValue - minValue), 1);
+
+    const formatFunc = getValueFormat(unit);
+    const valueFormatted = formatFunc(numericValue, decimals);
+    const vertical = orientation === 'vertical';
+
+    return vertical
+      ? this.renderVerticalBar(valueFormatted, valuePercent)
+      : this.renderHorizontalLCD(valueFormatted, valuePercent);
+  }
+}
+
+interface BarColors {
+  value: string;
+  bar: string;
+  border: string;
+}

+ 9 - 0
packages/grafana-ui/src/components/BarGauge/_BarGauge.scss

@@ -0,0 +1,9 @@
+.bar-gauge {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+}
+
+.bar-gauge__value {
+  text-align: center;
+}

+ 358 - 0
packages/grafana-ui/src/components/BarGauge/__snapshots__/BarGauge.test.tsx.snap

@@ -0,0 +1,358 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Render BarGauge with basic options should render 1`] = `
+<div
+  style={
+    Object {
+      "alignItems": "center",
+      "display": "flex",
+      "flexDirection": "row",
+      "height": "300px",
+      "width": "300px",
+    }
+  }
+>
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.7)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    style={
+      Object {
+        "backgroundColor": "rgba(126, 178, 109, 0.15)",
+        "borderRadius": "2px",
+        "height": "300px",
+        "marginRight": "4px",
+        "width": "4px",
+      }
+    }
+  />
+  <div
+    className="bar-gauge__value"
+    style={
+      Object {
+        "color": "#7EB26D",
+        "fontSize": "27.272727272727263px",
+        "marginLeft": "8px",
+      }
+    }
+  >
+    25
+  </div>
+</div>
+`;

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

@@ -39,7 +39,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
     this.props.onChange(color);
   };
 
-  handleChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
+  onChange = (event: React.SyntheticEvent<HTMLInputElement>) => {
     const newColor = tinycolor(event.currentTarget.value);
 
     this.setState({
@@ -51,7 +51,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
     }
   };
 
-  handleBlur = () => {
+  onBlur = () => {
     const newColor = tinycolor(this.state.value);
 
     if (!newColor.isValid()) {
@@ -84,7 +84,7 @@ class ColorInput extends React.PureComponent<ColorInputProps, ColorInputState> {
             flexGrow: 1,
           }}
         >
-          <input className="gf-form-input" value={value} onChange={this.handleChange} onBlur={this.handleBlur} />
+          <input className="gf-form-input" value={value} onChange={this.onChange} onBlur={this.onBlur} />
         </div>
       </div>
     );

+ 10 - 1
packages/grafana-ui/src/components/ColorPicker/ColorPicker.story.tsx

@@ -50,7 +50,16 @@ ColorPickerStories.add('Series color picker', () => {
             color={selectedColor}
             onChange={color => updateSelectedColor(color)}
           >
-            <div style={{ color: selectedColor, cursor: 'pointer' }}>Open color picker</div>
+            {({ ref, showColorPicker, hideColorPicker }) => (
+              <div
+                ref={ref}
+                onMouseLeave={hideColorPicker}
+                onClick={showColorPicker}
+                style={{ color: selectedColor, cursor: 'pointer' }}
+              >
+                Open color picker
+              </div>
+            )}
           </SeriesColorPicker>
         );
       }}

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

@@ -0,0 +1,23 @@
+import React from 'react';
+import renderer from 'react-test-renderer';
+import { ColorPicker } from './ColorPicker';
+import { ColorPickerTrigger } from './ColorPickerTrigger';
+
+describe('ColorPicker', () => {
+  it('renders ColorPickerTrigger component by default', () => {
+    expect(
+      renderer.create(<ColorPicker color="#EAB839" onChange={() => {}} />).root.findByType(ColorPickerTrigger)
+    ).toBeTruthy();
+  });
+
+  it('renders custom trigger when supplied', () => {
+    const div = renderer
+      .create(
+        <ColorPicker color="#EAB839" onChange={() => {}}>
+          {() => <div>Custom trigger</div>}
+        </ColorPicker>
+      )
+      .root.findByType('div');
+    expect(div.children[0]).toBe('Custom trigger');
+  });
+});

+ 31 - 21
packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx

@@ -1,4 +1,5 @@
 import React, { Component, createRef } from 'react';
+import { omit } from 'lodash';
 import { PopperController } from '../Tooltip/PopperController';
 import { Popper } from '../Tooltip/Popper';
 import { ColorPickerPopover, ColorPickerProps, ColorPickerChangeHandler } from './ColorPickerPopover';
@@ -6,16 +7,31 @@ import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
 import { SeriesColorPickerPopover } from './SeriesColorPickerPopover';
 
 import { withTheme } from '../../themes/ThemeContext';
+import { ColorPickerTrigger } from './ColorPickerTrigger';
+
+/**
+ * If you need custom trigger for the color picker you can do that with a render prop pattern and supply a function
+ * as a child. You will get show/hide function which you can map to desired interaction (like onClick or onMouseLeave)
+ * and a ref which needs to be passed to an HTMLElement for correct positioning. If you want to use class or functional
+ * component as a custom trigger you will need to forward the reference to first HTMLElement child.
+ */
+type ColorPickerTriggerRenderer = (props: {
+  // This should be a React.RefObject<HTMLElement> but due to how object refs are defined you cannot downcast from that
+  // to a specific type like React.RefObject<HTMLDivElement> even though it would be fine in runtime.
+  ref: React.RefObject<any>;
+  showColorPicker: () => void;
+  hideColorPicker: () => void;
+}) => React.ReactNode;
 
 export const colorPickerFactory = <T extends ColorPickerProps>(
   popover: React.ComponentType<T>,
   displayName = 'ColorPicker'
 ) => {
-  return class ColorPicker extends Component<T, any> {
+  return class ColorPicker extends Component<T & { children?: ColorPickerTriggerRenderer }, any> {
     static displayName = displayName;
-    pickerTriggerRef = createRef<HTMLDivElement>();
+    pickerTriggerRef = createRef<any>();
 
-    handleColorChange = (color: string) => {
+    onColorChange = (color: string) => {
       const { onColorChange, onChange } = this.props;
       const changeHandler = (onColorChange || onChange) as ColorPickerChangeHandler;
 
@@ -23,11 +39,11 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
     };
 
     render() {
+      const { theme, children } = this.props;
       const popoverElement = React.createElement(popover, {
-        ...this.props,
-        onChange: this.handleColorChange,
+        ...omit(this.props, 'children'),
+        onChange: this.onColorChange,
       });
-      const { theme, children } = this.props;
 
       return (
         <PopperController content={popoverElement} hideAfter={300}>
@@ -45,27 +61,21 @@ export const colorPickerFactory = <T extends ColorPickerProps>(
                 )}
 
                 {children ? (
-                  React.cloneElement(children as JSX.Element, {
+                  // Children have a bit weird type due to intersection used in the definition so we need to cast here,
+                  // but the definition is correct and should not allow to pass a children that does not conform to
+                  // ColorPickerTriggerRenderer type.
+                  (children as ColorPickerTriggerRenderer)({
                     ref: this.pickerTriggerRef,
-                    onClick: showPopper,
-                    onMouseLeave: hidePopper,
+                    showColorPicker: showPopper,
+                    hideColorPicker: hidePopper,
                   })
                 ) : (
-                  <div
+                  <ColorPickerTrigger
                     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.type),
-                        }}
-                      />
-                    </div>
-                  </div>
+                    color={getColorFromHexRgbOrName(this.props.color || '#000000', theme.type)}
+                  />
                 )}
               </>
             );

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

@@ -17,8 +17,8 @@ export interface ColorPickerProps extends Themeable {
    */
   onColorChange?: ColorPickerChangeHandler;
   enableNamedColors?: boolean;
-  children?: JSX.Element;
 }
+
 export interface Props<T> extends ColorPickerProps, PopperContentProps {
   customPickers?: T;
 }
@@ -60,7 +60,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
     changeHandler(getColorFromHexRgbOrName(color, theme.type));
   };
 
-  handleTabChange = (tab: PickerType | keyof T) => {
+  onTabChange = (tab: PickerType | keyof T) => {
     return () => this.setState({ activePicker: tab });
   };
 
@@ -104,7 +104,7 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
       <>
         {Object.keys(customPickers).map(key => {
           return (
-            <div className={this.getTabClassName(key)} onClick={this.handleTabChange(key)} key={key}>
+            <div className={this.getTabClassName(key)} onClick={this.onTabChange(key)} key={key}>
               {customPickers[key].name}
             </div>
           );
@@ -119,10 +119,10 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
     return (
       <div className={`ColorPickerPopover ColorPickerPopover--${colorPickerTheme}`}>
         <div className="ColorPickerPopover__tabs">
-          <div className={this.getTabClassName('palette')} onClick={this.handleTabChange('palette')}>
+          <div className={this.getTabClassName('palette')} onClick={this.onTabChange('palette')}>
             Colors
           </div>
-          <div className={this.getTabClassName('spectrum')} onClick={this.handleTabChange('spectrum')}>
+          <div className={this.getTabClassName('spectrum')} onClick={this.onTabChange('spectrum')}>
             Custom
           </div>
           {this.renderCustomPickerTabs()}

+ 56 - 0
packages/grafana-ui/src/components/ColorPicker/ColorPickerTrigger.tsx

@@ -0,0 +1,56 @@
+import React, { forwardRef } from 'react';
+
+interface ColorPickerTriggerProps {
+  onClick: () => void;
+  onMouseLeave: () => void;
+  color: string;
+}
+
+export const ColorPickerTrigger = forwardRef(function ColorPickerTrigger(
+  props: ColorPickerTriggerProps,
+  ref: React.Ref<HTMLDivElement>
+) {
+  return (
+    <div
+      ref={ref}
+      onClick={props.onClick}
+      onMouseLeave={props.onMouseLeave}
+      style={{
+        overflow: 'hidden',
+        background: 'inherit',
+        border: 'none',
+        color: 'inherit',
+        padding: 0,
+        borderRadius: 10,
+        cursor: 'pointer',
+      }}
+    >
+      <div
+        style={{
+          position: 'relative',
+          width: 15,
+          height: 15,
+          border: 'none',
+          margin: 0,
+          float: 'left',
+          zIndex: 0,
+          backgroundImage:
+            // tslint:disable-next-line:max-line-length
+            'url(data:image/png,base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAAMCAIAAADZF8uwAAAAGUlEQVQYV2M4gwH+YwCGIasIUwhT25BVBADtzYNYrHvv4gAAAABJRU5ErkJggg==)',
+        }}
+      >
+        <div
+          style={{
+            backgroundColor: props.color,
+            display: 'block',
+            position: 'absolute',
+            top: 0,
+            left: 0,
+            bottom: 0,
+            right: 0,
+          }}
+        />
+      </div>
+    </div>
+  );
+});

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

@@ -8,7 +8,7 @@ import { renderComponentWithTheme } from '../../utils/storybook/withTheme';
 import { UseState } from '../../utils/storybook/UseState';
 
 const BasicGreen = getColorDefinitionByName('green');
-const BasicBlue = getColorDefinitionByName('blue');
+const BasicRed = getColorDefinitionByName('red');
 const LightBlue = getColorDefinitionByName('light-blue');
 
 const NamedColorsPaletteStories = storiesOf('UI/ColorPicker/Palettes/NamedColorsPalette', module);
@@ -41,7 +41,7 @@ NamedColorsPaletteStories.add('Named colors swatch - support for named colors',
     'Selected color',
     {
       Green: BasicGreen.variants.dark,
-      Red: BasicBlue.variants.dark,
+      Red: BasicRed.variants.dark,
       'Light blue': LightBlue.variants.dark,
     },
     'red'

+ 0 - 53
packages/grafana-ui/src/components/ColorPicker/_ColorPicker.scss

@@ -161,59 +161,6 @@ $arrowSize: 15px;
   flex-grow: 1;
 }
 
-.sp-replacer {
-  background: inherit;
-  border: none;
-  color: inherit;
-  padding: 0;
-  border-radius: 10px;
-  cursor: pointer;
-}
-
-.sp-replacer:hover,
-.sp-replacer.sp-active {
-  border-color: inherit;
-  color: inherit;
-}
-
-.sp-container {
-  border-radius: 0;
-  background-color: $dropdownBackground;
-  border: none;
-  padding: 0;
-}
-
-.sp-palette-container,
-.sp-picker-container {
-  border: none;
-}
-
-.sp-dd {
-  display: none;
-}
-
-.sp-preview {
-  position: relative;
-  width: 15px;
-  height: 15px;
-  border: none;
-  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: $arrowSize;
   padding-left: 6px;

+ 2 - 2
packages/grafana-ui/src/components/ColorPicker/warnAboutColorPickerPropsDeprecation.ts

@@ -1,9 +1,9 @@
-import propDeprecationWarning from '../../utils/propDeprecationWarning';
+import deprecationWarning from '../../utils/deprecationWarning';
 import { ColorPickerProps } from './ColorPickerPopover';
 
 export const warnAboutColorPickerPropsDeprecation = (componentName: string, props: ColorPickerProps) => {
   const { onColorChange } = props;
   if (onColorChange) {
-    propDeprecationWarning(componentName, 'onColorChange', 'onChange');
+    deprecationWarning(componentName, 'onColorChange', 'onChange');
   }
 };

+ 15 - 0
packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx

@@ -15,6 +15,7 @@ interface Props {
   scrollTop?: number;
   setScrollTop: (event: any) => void;
   autoHeightMin?: number | string;
+  updateAfterMountMs?: number;
 }
 
 /**
@@ -48,6 +49,20 @@ export class CustomScrollbar extends Component<Props> {
 
   componentDidMount() {
     this.updateScroll();
+
+    // this logic is to make scrollbar visible when content is added body after mount
+    if (this.props.updateAfterMountMs) {
+      setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs);
+    }
+  }
+
+  updateAfterMount() {
+    if (this.ref && this.ref.current) {
+      const scrollbar = this.ref.current as any;
+      if (scrollbar.update) {
+        scrollbar.update();
+      }
+    }
   }
 
   componentDidUpdate() {

+ 11 - 18
packages/grafana-ui/src/components/DeleteButton/DeleteButton.story.tsx

@@ -1,24 +1,17 @@
-import React, { FunctionComponent } from 'react';
+import React from 'react';
 import { storiesOf } from '@storybook/react';
 import { DeleteButton } from './DeleteButton';
-
-const CenteredStory: FunctionComponent<{}> = ({ children }) => {
-  return (
-    <div
-      style={{
-        height: '100vh  ',
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-      }}
-    >
-      {children}
-    </div>
-  );
-};
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+import { action } from '@storybook/addon-actions';
 
 storiesOf('UI/DeleteButton', module)
-  .addDecorator(story => <CenteredStory>{story()}</CenteredStory>)
+  .addDecorator(withCenteredStory)
   .add('default', () => {
-    return <DeleteButton onConfirm={() => {}} />;
+    return (
+      <DeleteButton
+        onConfirm={() => {
+          action('Delete Confirmed')('delete!');
+        }}
+      />
+    );
   });

+ 1 - 1
packages/grafana-ui/src/components/FormField/_FormField.scss

@@ -1,5 +1,5 @@
 .form-field {
-  margin-bottom: $gf-form-margin;
+  margin-bottom: $space-xxs;
   display: flex;
   flex-direction: row;
   align-items: center;

+ 22 - 37
packages/grafana-ui/src/components/Gauge/Gauge.tsx

@@ -1,12 +1,12 @@
 import React, { PureComponent } from 'react';
 import $ from 'jquery';
+
+import { ValueMapping, Threshold, GrafanaThemeType } from '../../types';
 import { getMappedValue } from '../../utils/valueMappings';
-import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
-import { Themeable, GrafanaThemeType } from '../../types/theme';
-import { ValueMapping, Threshold, BasicGaugeColor } from '../../types/panel';
-import { getValueFormat } from '../../utils/valueFormats/valueFormats';
+import { getColorFromHexRgbOrName, getValueFormat, getThresholdForValue } from '../../utils';
+import { Themeable } from '../../index';
 
-type TimeSeriesValue = string | number | null;
+type GaugeValue = string | number | null;
 
 export interface Props extends Themeable {
   decimals?: number | null;
@@ -30,7 +30,7 @@ const FONT_SCALE = 1;
 export class Gauge extends PureComponent<Props> {
   canvasElement: any;
 
-  static defaultProps = {
+  static defaultProps: Partial<Props> = {
     maxValue: 100,
     valueMappings: [],
     minValue: 0,
@@ -41,7 +41,6 @@ export class Gauge extends PureComponent<Props> {
     thresholds: [],
     unit: 'none',
     stat: 'avg',
-    theme: GrafanaThemeType.Dark,
   };
 
   componentDidMount() {
@@ -52,7 +51,7 @@ export class Gauge extends PureComponent<Props> {
     this.draw();
   }
 
-  formatValue(value: TimeSeriesValue) {
+  formatValue(value: GaugeValue) {
     const { decimals, valueMappings, prefix, suffix, unit } = this.props;
 
     if (isNaN(value as number)) {
@@ -73,26 +72,16 @@ export class Gauge extends PureComponent<Props> {
     return `${prefix && prefix + ' '}${handleNoValueValue}${suffix && ' ' + suffix}`;
   }
 
-  getFontColor(value: TimeSeriesValue) {
+  getFontColor(value: GaugeValue): string {
     const { thresholds, theme } = this.props;
 
-    if (thresholds.length === 1) {
-      return getColorFromHexRgbOrName(thresholds[0].color, theme.type);
-    }
-
-    const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
-    if (atThreshold) {
-      return getColorFromHexRgbOrName(atThreshold.color, theme.type);
-    }
-
-    const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+    const activeThreshold = getThresholdForValue(thresholds, value);
 
-    if (belowThreshold.length > 0) {
-      const nearestThreshold = belowThreshold.sort((t1, t2) => t2.value - t1.value)[0];
-      return getColorFromHexRgbOrName(nearestThreshold.color, theme.type);
+    if (activeThreshold !== null) {
+      return getColorFromHexRgbOrName(activeThreshold.color, theme.type);
     }
 
-    return BasicGaugeColor.Red;
+    return '';
   }
 
   getFormattedThresholds() {
@@ -134,7 +123,7 @@ export class Gauge extends PureComponent<Props> {
       Math.min(dimension / 5, 100) * (formattedValue !== null ? this.getFontScale(formattedValue.length) : 1);
     const thresholdLabelFontSize = fontSize / 2.5;
 
-    const options = {
+    const options: any = {
       series: {
         gauges: {
           gauge: {
@@ -184,19 +173,15 @@ export class Gauge extends PureComponent<Props> {
     const { height, width } = this.props;
 
     return (
-      <div className="singlestat-panel">
-        <div
-          style={{
-            height: `${height * 0.9}px`,
-            width: `${Math.min(width, height * 1.3)}px`,
-            top: '10px',
-            margin: 'auto',
-          }}
-          ref={element => (this.canvasElement = element)}
-        />
-      </div>
+      <div
+        style={{
+          height: `${Math.min(height, width * 1.3)}px`,
+          width: `${Math.min(width, height * 1.3)}px`,
+          top: '10px',
+          margin: 'auto',
+        }}
+        ref={element => (this.canvasElement = element)}
+      />
     );
   }
 }
-
-export default Gauge;

+ 1 - 1
packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss

@@ -53,7 +53,7 @@
 }
 
 .panel-options-group__title {
-  font-size: 1.1rem;
+  font-size: 16px;
   position: relative;
   top: 1px;
 }

+ 1 - 1
packages/grafana-ui/src/components/Select/_Select.scss

@@ -3,7 +3,7 @@ $select-input-bg-disabled: $input-bg-disabled;
 
 @mixin select-control() {
   width: 100%;
-  margin-right: $gf-form-margin;
+  margin-right: $space-xs;
   @include border-radius($input-border-radius-sm);
   background-color: $input-bg;
 }

+ 99 - 0
packages/grafana-ui/src/components/Table/Table.story.tsx

@@ -0,0 +1,99 @@
+// import React from 'react';
+import { storiesOf } from '@storybook/react';
+import { Table } from './Table';
+import { getTheme } from '../../themes';
+
+import { migratedTestTable, migratedTestStyles, simpleTable } from './examples';
+import { ScopedVars, TableData, GrafanaThemeType } from '../../types/index';
+import { withFullSizeStory } from '../../utils/storybook/withFullSizeStory';
+import { number, boolean } from '@storybook/addon-knobs';
+
+const replaceVariables = (value: string, scopedVars?: ScopedVars) => {
+  if (scopedVars) {
+    // For testing variables replacement in link
+    for (const key in scopedVars) {
+      const val = scopedVars[key];
+      value = value.replace('$' + key, val.value);
+    }
+  }
+  return value;
+};
+
+export function columnIndexToLeter(column: number) {
+  const A = 'A'.charCodeAt(0);
+  const c1 = Math.floor(column / 26);
+  const c2 = column % 26;
+  if (c1 > 0) {
+    return String.fromCharCode(A + c1 - 1) + String.fromCharCode(A + c2);
+  }
+  return String.fromCharCode(A + c2);
+}
+
+export function makeDummyTable(columnCount: number, rowCount: number): TableData {
+  return {
+    columns: Array.from(new Array(columnCount), (x, i) => {
+      return {
+        text: columnIndexToLeter(i),
+      };
+    }),
+    rows: Array.from(new Array(rowCount), (x, rowId) => {
+      const suffix = (rowId + 1).toString();
+      return Array.from(new Array(columnCount), (x, colId) => columnIndexToLeter(colId) + suffix);
+    }),
+    type: 'table',
+    columnMap: {},
+  };
+}
+
+storiesOf('Alpha/Table', module)
+  .add('Basic Table', () => {
+    // NOTE: This example does not seem to survice rotate &
+    // Changing fixed headers... but the next one does?
+    // perhaps `simpleTable` is static and reused?
+
+    const showHeader = boolean('Show Header', true);
+    const fixedHeader = boolean('Fixed Header', true);
+    const fixedColumns = number('Fixed Columns', 0, { min: 0, max: 50, step: 1, range: false });
+    const rotate = boolean('Rotate', false);
+
+    return withFullSizeStory(Table, {
+      styles: [],
+      data: simpleTable,
+      replaceVariables,
+      showHeader,
+      fixedHeader,
+      fixedColumns,
+      rotate,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  })
+  .add('Variable Size', () => {
+    const columnCount = number('Column Count', 15, { min: 2, max: 50, step: 1, range: false });
+    const rowCount = number('Row Count', 20, { min: 0, max: 100, step: 1, range: false });
+
+    const showHeader = boolean('Show Header', true);
+    const fixedHeader = boolean('Fixed Header', true);
+    const fixedColumns = number('Fixed Columns', 1, { min: 0, max: 50, step: 1, range: false });
+    const rotate = boolean('Rotate', false);
+
+    return withFullSizeStory(Table, {
+      styles: [],
+      data: makeDummyTable(columnCount, rowCount),
+      replaceVariables,
+      showHeader,
+      fixedHeader,
+      fixedColumns,
+      rotate,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  })
+  .add('Test Config (migrated)', () => {
+    return withFullSizeStory(Table, {
+      styles: migratedTestStyles,
+      data: migratedTestTable,
+      replaceVariables,
+      showHeader: true,
+      rotate: true,
+      theme: getTheme(GrafanaThemeType.Light),
+    });
+  });

+ 287 - 0
packages/grafana-ui/src/components/Table/Table.tsx

@@ -0,0 +1,287 @@
+// Libraries
+import _ from 'lodash';
+import React, { Component, ReactElement } from 'react';
+import {
+  SortDirectionType,
+  SortIndicator,
+  MultiGrid,
+  CellMeasurerCache,
+  CellMeasurer,
+  GridCellProps,
+} from 'react-virtualized';
+import { Themeable } from '../../types/theme';
+
+import { sortTableData } from '../../utils/processTableData';
+
+import { TableData, InterpolateFunction } from '@grafana/ui';
+import {
+  TableCellBuilder,
+  ColumnStyle,
+  getCellBuilder,
+  TableCellBuilderOptions,
+  simpleCellBuilder,
+} from './TableCellBuilder';
+import { stringToJsRegex } from '../../utils/index';
+
+export interface Props extends Themeable {
+  data: TableData;
+
+  showHeader: boolean;
+  fixedHeader: boolean;
+  fixedColumns: number;
+  rotate: boolean;
+  styles: ColumnStyle[];
+
+  replaceVariables: InterpolateFunction;
+  width: number;
+  height: number;
+  isUTC?: boolean;
+}
+
+interface State {
+  sortBy?: number;
+  sortDirection?: SortDirectionType;
+  data: TableData;
+}
+
+interface ColumnRenderInfo {
+  header: string;
+  builder: TableCellBuilder;
+}
+
+interface DataIndex {
+  column: number;
+  row: number; // -1 is the header!
+}
+
+export class Table extends Component<Props, State> {
+  renderer: ColumnRenderInfo[];
+  measurer: CellMeasurerCache;
+  scrollToTop = false;
+
+  static defaultProps = {
+    showHeader: true,
+    fixedHeader: true,
+    fixedColumns: 0,
+    rotate: false,
+  };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      data: props.data,
+    };
+
+    this.renderer = this.initColumns(props);
+    this.measurer = new CellMeasurerCache({
+      defaultHeight: 30,
+      defaultWidth: 150,
+    });
+  }
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    const { data, styles, showHeader } = this.props;
+    const { sortBy, sortDirection } = this.state;
+    const dataChanged = data !== prevProps.data;
+    const configsChanged =
+      showHeader !== prevProps.showHeader ||
+      this.props.rotate !== prevProps.rotate ||
+      this.props.fixedColumns !== prevProps.fixedColumns ||
+      this.props.fixedHeader !== prevProps.fixedHeader;
+
+    // Reset the size cache
+    if (dataChanged || configsChanged) {
+      this.measurer.clearAll();
+    }
+
+    // Update the renderer if options change
+    // We only *need* do to this if the header values changes, but this does every data update
+    if (dataChanged || styles !== prevProps.styles) {
+      this.renderer = this.initColumns(this.props);
+    }
+
+    // Update the data when data or sort changes
+    if (dataChanged || sortBy !== prevState.sortBy || sortDirection !== prevState.sortDirection) {
+      this.scrollToTop = true;
+      this.setState({ data: sortTableData(data, sortBy, sortDirection === 'DESC') });
+    }
+  }
+
+  /** Given the configuration, setup how each column gets rendered */
+  initColumns(props: Props): ColumnRenderInfo[] {
+    const { styles, data } = props;
+
+    return data.columns.map((col, index) => {
+      let title = col.text;
+      let style: ColumnStyle | null = null; // ColumnStyle
+
+      // Find the style based on the text
+      for (let i = 0; i < styles.length; i++) {
+        const s = styles[i];
+        const regex = stringToJsRegex(s.pattern);
+        if (title.match(regex)) {
+          style = s;
+          if (s.alias) {
+            title = title.replace(regex, s.alias);
+          }
+          break;
+        }
+      }
+
+      return {
+        header: title,
+        builder: getCellBuilder(col, style, this.props),
+      };
+    });
+  }
+
+  //----------------------------------------------------------------------
+  //----------------------------------------------------------------------
+
+  doSort = (columnIndex: number) => {
+    let sort: any = this.state.sortBy;
+    let dir = this.state.sortDirection;
+    if (sort !== columnIndex) {
+      dir = 'DESC';
+      sort = columnIndex;
+    } else if (dir === 'DESC') {
+      dir = 'ASC';
+    } else {
+      sort = null;
+    }
+    this.setState({ sortBy: sort, sortDirection: dir });
+  };
+
+  /** Converts the grid coordinates to TableData coordinates */
+  getCellRef = (rowIndex: number, columnIndex: number): DataIndex => {
+    const { showHeader, rotate } = this.props;
+    const rowOffset = showHeader ? -1 : 0;
+
+    if (rotate) {
+      return { column: rowIndex, row: columnIndex + rowOffset };
+    } else {
+      return { column: columnIndex, row: rowIndex + rowOffset };
+    }
+  };
+
+  onCellClick = (rowIndex: number, columnIndex: number) => {
+    const { row, column } = this.getCellRef(rowIndex, columnIndex);
+    if (row < 0) {
+      this.doSort(column);
+    } else {
+      const values = this.state.data.rows[row];
+      const value = values[column];
+      console.log('CLICK', value, row);
+    }
+  };
+
+  headerBuilder = (cell: TableCellBuilderOptions): ReactElement<'div'> => {
+    const { data, sortBy, sortDirection } = this.state;
+    const { columnIndex, rowIndex, style } = cell.props;
+    const { column } = this.getCellRef(rowIndex, columnIndex);
+
+    let col = data.columns[column];
+    const sorting = sortBy === column;
+    if (!col) {
+      col = {
+        text: '??' + columnIndex + '???',
+      };
+    }
+
+    return (
+      <div className="gf-table-header" style={style} onClick={() => this.onCellClick(rowIndex, columnIndex)}>
+        {col.text}
+        {sorting && <SortIndicator sortDirection={sortDirection} />}
+      </div>
+    );
+  };
+
+  getTableCellBuilder = (column: number): TableCellBuilder => {
+    const render = this.renderer[column];
+    if (render && render.builder) {
+      return render.builder;
+    }
+    return simpleCellBuilder; // the default
+  };
+
+  cellRenderer = (props: GridCellProps): React.ReactNode => {
+    const { rowIndex, columnIndex, key, parent } = props;
+    const { row, column } = this.getCellRef(rowIndex, columnIndex);
+    const { data } = this.state;
+
+    const isHeader = row < 0;
+    const rowData = isHeader ? data.columns : data.rows[row];
+    const value = rowData ? rowData[column] : '';
+    const builder = isHeader ? this.headerBuilder : this.getTableCellBuilder(column);
+
+    return (
+      <CellMeasurer cache={this.measurer} columnIndex={columnIndex} key={key} parent={parent} rowIndex={rowIndex}>
+        {builder({
+          value,
+          row: rowData,
+          column: data.columns[column],
+          table: this,
+          props,
+        })}
+      </CellMeasurer>
+    );
+  };
+
+  render() {
+    const { showHeader, fixedHeader, fixedColumns, rotate, width, height } = this.props;
+    const { data } = this.state;
+
+    let columnCount = data.columns.length;
+    let rowCount = data.rows.length + (showHeader ? 1 : 0);
+
+    let fixedColumnCount = Math.min(fixedColumns, columnCount);
+    let fixedRowCount = showHeader && fixedHeader ? 1 : 0;
+
+    if (rotate) {
+      const temp = columnCount;
+      columnCount = rowCount;
+      rowCount = temp;
+
+      fixedRowCount = 0;
+      fixedColumnCount = Math.min(fixedColumns, rowCount) + (showHeader && fixedHeader ? 1 : 0);
+    }
+
+    // Called after sort or the data changes
+    const scroll = this.scrollToTop ? 1 : -1;
+    const scrollToRow = rotate ? -1 : scroll;
+    const scrollToColumn = rotate ? scroll : -1;
+    if (this.scrollToTop) {
+      this.scrollToTop = false;
+    }
+
+    return (
+      <MultiGrid
+        {
+          ...this.state /** Force MultiGrid to update when data changes */
+        }
+        {
+          ...this.props /** Force MultiGrid to update when data changes */
+        }
+        scrollToRow={scrollToRow}
+        columnCount={columnCount}
+        scrollToColumn={scrollToColumn}
+        rowCount={rowCount}
+        overscanColumnCount={8}
+        overscanRowCount={8}
+        columnWidth={this.measurer.columnWidth}
+        deferredMeasurementCache={this.measurer}
+        cellRenderer={this.cellRenderer}
+        rowHeight={this.measurer.rowHeight}
+        width={width}
+        height={height}
+        fixedColumnCount={fixedColumnCount}
+        fixedRowCount={fixedRowCount}
+        classNameTopLeftGrid="gf-table-fixed-column"
+        classNameBottomLeftGrid="gf-table-fixed-column"
+      />
+    );
+  }
+}
+
+export default Table;

+ 291 - 0
packages/grafana-ui/src/components/Table/TableCellBuilder.tsx

@@ -0,0 +1,291 @@
+// Libraries
+import _ from 'lodash';
+import React, { ReactElement } from 'react';
+import { GridCellProps } from 'react-virtualized';
+import { Table, Props } from './Table';
+import moment from 'moment';
+import { ValueFormatter } from '../../utils/index';
+import { GrafanaTheme } from '../../types/theme';
+import { getValueFormat, getColorFromHexRgbOrName, Column } from '@grafana/ui';
+import { InterpolateFunction } from '../../types/panel';
+
+export interface TableCellBuilderOptions {
+  value: any;
+  column?: Column;
+  row?: any[];
+  table?: Table;
+  className?: string;
+  props: GridCellProps;
+}
+
+export type TableCellBuilder = (cell: TableCellBuilderOptions) => ReactElement<'div'>;
+
+/** Simplest cell that just spits out the value */
+export const simpleCellBuilder: TableCellBuilder = (cell: TableCellBuilderOptions) => {
+  const { props, value, className } = cell;
+  const { style } = props;
+
+  return (
+    <div style={style} className={'gf-table-cell ' + className}>
+      {value}
+    </div>
+  );
+};
+
+// ***************************************************************************
+// HERE BE DRAGONS!!!
+// ***************************************************************************
+//
+//  The following code has been migrated blindy two times from the angular
+//  table panel.  I don't understand all the options nor do I know if they
+//  are correct!
+//
+// ***************************************************************************
+
+// Made to match the existing (untyped) settings in the angular table
+export interface ColumnStyle {
+  pattern: string;
+
+  alias?: string;
+  colorMode?: 'cell' | 'value';
+  colors?: any[];
+  decimals?: number;
+  thresholds?: any[];
+  type?: 'date' | 'number' | 'string' | 'hidden';
+  unit?: string;
+  dateFormat?: string;
+  sanitize?: boolean; // not used in react
+  mappingType?: any;
+  valueMaps?: any;
+  rangeMaps?: any;
+
+  link?: any;
+  linkUrl?: any;
+  linkTooltip?: any;
+  linkTargetBlank?: boolean;
+
+  preserveFormat?: boolean;
+}
+
+// private mapper:ValueMapper,
+// private style:ColumnStyle,
+// private theme:GrafanaTheme,
+// private column:Column,
+// private replaceVariables: InterpolateFunction,
+// private fmt?:ValueFormatter) {
+
+export function getCellBuilder(schema: Column, style: ColumnStyle | null, props: Props): TableCellBuilder {
+  if (!style) {
+    return simpleCellBuilder;
+  }
+
+  if (style.type === 'hidden') {
+    // TODO -- for hidden, we either need to:
+    // 1. process the Table and remove hidden fields
+    // 2. do special math to pick the right column skipping hidden fields
+    throw new Error('hidden not supported!');
+  }
+
+  if (style.type === 'date') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === undefined || v === null) {
+          return '-';
+        }
+
+        if (_.isArray(v)) {
+          v = v[0];
+        }
+        let date = moment(v);
+        if (false) {
+          // TODO?????? this.props.isUTC) {
+          date = date.utc();
+        }
+        return date.format(style.dateFormat);
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+  }
+
+  if (style.type === 'string') {
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (_.isArray(v)) {
+          v = v.join(', ');
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables
+    ).build;
+    // TODO!!!!  all the mapping stuff!!!!
+  }
+
+  if (style.type === 'number') {
+    const valueFormatter = getValueFormat(style.unit || schema.unit || 'none');
+    return new CellBuilderWithStyle(
+      (v: any) => {
+        if (v === null || v === void 0) {
+          return '-';
+        }
+        return v;
+      },
+      style,
+      props.theme,
+      schema,
+      props.replaceVariables,
+      valueFormatter
+    ).build;
+  }
+
+  return simpleCellBuilder;
+}
+
+type ValueMapper = (value: any) => any;
+
+// Runs the value through a formatter and adds colors to the cell properties
+class CellBuilderWithStyle {
+  constructor(
+    private mapper: ValueMapper,
+    private style: ColumnStyle,
+    private theme: GrafanaTheme,
+    private column: Column,
+    private replaceVariables: InterpolateFunction,
+    private fmt?: ValueFormatter
+  ) {
+    //
+    console.log('COLUMN', column.text, theme);
+  }
+
+  getColorForValue = (value: any): string | null => {
+    const { thresholds, colors } = this.style;
+    if (!thresholds || !colors) {
+      return null;
+    }
+
+    for (let i = thresholds.length; i > 0; i--) {
+      if (value >= thresholds[i - 1]) {
+        return getColorFromHexRgbOrName(colors[i], this.theme.type);
+      }
+    }
+    return getColorFromHexRgbOrName(_.first(colors), this.theme.type);
+  };
+
+  build = (cell: TableCellBuilderOptions) => {
+    let { props } = cell;
+    let value = this.mapper(cell.value);
+
+    if (_.isNumber(value)) {
+      if (this.fmt) {
+        value = this.fmt(value, this.style.decimals);
+      }
+
+      // For numeric values set the color
+      const { colorMode } = this.style;
+      if (colorMode) {
+        const color = this.getColorForValue(Number(value));
+        if (color) {
+          if (colorMode === 'cell') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                backgroundColor: color,
+                color: 'white',
+              },
+            };
+          } else if (colorMode === 'value') {
+            props = {
+              ...props,
+              style: {
+                ...props.style,
+                color: color,
+              },
+            };
+          }
+        }
+      }
+    }
+
+    const cellClasses = [];
+    if (this.style.preserveFormat) {
+      cellClasses.push('table-panel-cell-pre');
+    }
+
+    if (this.style.link) {
+      // Render cell as link
+      const { row } = cell;
+
+      const scopedVars: any = {};
+      if (row) {
+        for (let i = 0; i < row.length; i++) {
+          scopedVars[`__cell_${i}`] = { value: row[i] };
+        }
+      }
+      scopedVars['__cell'] = { value: value };
+
+      const cellLink = this.replaceVariables(this.style.linkUrl, scopedVars, encodeURIComponent);
+      const cellLinkTooltip = this.replaceVariables(this.style.linkTooltip, scopedVars);
+      const cellTarget = this.style.linkTargetBlank ? '_blank' : '';
+
+      cellClasses.push('table-panel-cell-link');
+      value = (
+        <a
+          href={cellLink}
+          target={cellTarget}
+          data-link-tooltip
+          data-original-title={cellLinkTooltip}
+          data-placement="right"
+        >
+          {value}
+        </a>
+      );
+    }
+
+    // ??? I don't think this will still work!
+    if (this.column.filterable) {
+      cellClasses.push('table-panel-cell-filterable');
+      value = (
+        <>
+          {value}
+          <span>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter out value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="!="
+            >
+              <i className="fa fa-search-minus" />
+            </a>
+            <a
+              className="table-panel-filter-link"
+              data-link-tooltip
+              data-original-title="Filter for value"
+              data-placement="bottom"
+              data-row={props.rowIndex}
+              data-column={props.columnIndex}
+              data-operator="="
+            >
+              <i className="fa fa-search-plus" />
+            </a>
+          </span>
+        </>
+      );
+    }
+
+    let className;
+    if (cellClasses.length) {
+      className = cellClasses.join(' ');
+    }
+
+    return simpleCellBuilder({ value, props, className });
+  };
+}

+ 25 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.story.tsx

@@ -0,0 +1,25 @@
+import React from 'react';
+
+import { storiesOf } from '@storybook/react';
+import TableInputCSV from './TableInputCSV';
+import { action } from '@storybook/addon-actions';
+import { TableData } from '../../types/data';
+import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
+
+const TableInputStories = storiesOf('UI/Table/Input', module);
+
+TableInputStories.addDecorator(withCenteredStory);
+
+TableInputStories.add('default', () => {
+  return (
+    <div style={{ width: '90%', height: '90vh' }}>
+      <TableInputCSV
+        text={'a,b,c\n1,2,3'}
+        onTableParsed={(table: TableData, text: string) => {
+          console.log('Table', table, text);
+          action('Table')(table, text);
+        }}
+      />
+    </div>
+  );
+});

+ 22 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.test.tsx

@@ -0,0 +1,22 @@
+import React from 'react';
+
+import renderer from 'react-test-renderer';
+import TableInputCSV from './TableInputCSV';
+import { TableData } from '../../types/data';
+
+describe('TableInputCSV', () => {
+  it('renders correctly', () => {
+    const tree = renderer
+      .create(
+        <TableInputCSV
+          text={'a,b,c\n1,2,3'}
+          onTableParsed={(table: TableData, text: string) => {
+            // console.log('Table:', table, 'from:', text);
+          }}
+        />
+      )
+      .toJSON();
+    //expect(tree).toMatchSnapshot();
+    expect(tree).toBeDefined();
+  });
+});

+ 95 - 0
packages/grafana-ui/src/components/Table/TableInputCSV.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import debounce from 'lodash/debounce';
+import { parseCSV, TableParseOptions, TableParseDetails } from '../../utils/processTableData';
+import { TableData } from '../../types/data';
+import { AutoSizer } from 'react-virtualized';
+
+interface Props {
+  options?: TableParseOptions;
+  text: string;
+  onTableParsed: (table: TableData, text: string) => void;
+}
+
+interface State {
+  text: string;
+  table: TableData;
+  details: TableParseDetails;
+}
+
+/**
+ * Expects the container div to have size set and will fill it 100%
+ */
+class TableInputCSV extends React.PureComponent<Props, State> {
+  constructor(props: Props) {
+    super(props);
+
+    // Shoud this happen in onComponentMounted?
+    const { text, options, onTableParsed } = props;
+    const details = {};
+    const table = parseCSV(text, options, details);
+    this.state = {
+      text,
+      table,
+      details,
+    };
+    onTableParsed(table, text);
+  }
+
+  readCSV = debounce(() => {
+    const details = {};
+    const table = parseCSV(this.state.text, this.props.options, details);
+    this.setState({ table, details });
+  }, 150);
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    const { text } = this.state;
+    if (text !== prevState.text || this.props.options !== prevProps.options) {
+      this.readCSV();
+    }
+    // If the props text has changed, replace our local version
+    if (this.props.text !== prevProps.text && this.props.text !== text) {
+      this.setState({ text: this.props.text });
+    }
+
+    if (this.state.table !== prevState.table) {
+      this.props.onTableParsed(this.state.table, this.state.text);
+    }
+  }
+
+  onFooterClicked = (event: any) => {
+    console.log('Errors', this.state);
+    const message = this.state.details
+      .errors!.map(err => {
+        return err.message;
+      })
+      .join('\n');
+    alert('CSV Parsing Errors:\n' + message);
+  };
+
+  onTextChange = (event: any) => {
+    this.setState({ text: event.target.value });
+  };
+
+  render() {
+    const { table, details } = this.state;
+
+    const hasErrors = details.errors && details.errors.length > 0;
+    const footerClassNames = hasErrors ? 'gf-table-input-csv-err' : '';
+
+    return (
+      <AutoSizer>
+        {({ height, width }) => (
+          <div className="gf-table-input-csv" style={{ width, height }}>
+            <textarea placeholder="Enter CSV here..." value={this.state.text} onChange={this.onTextChange} />
+            <footer onClick={this.onFooterClicked} className={footerClassNames}>
+              Rows:{table.rows.length}, Columns:{table.columns.length} &nbsp;
+              {hasErrors ? <i className="fa fa-exclamation-triangle" /> : <i className="fa fa-check-circle" />}
+            </footer>
+          </div>
+        )}
+      </AutoSizer>
+    );
+  }
+}
+
+export default TableInputCSV;

+ 80 - 0
packages/grafana-ui/src/components/Table/_Table.scss

@@ -0,0 +1,80 @@
+// .ReactVirtualized__Table {
+// }
+
+// .ReactVirtualized__Table__Grid {
+// }
+
+.ReactVirtualized__Table__headerRow {
+  font-weight: 700;
+  display: flex;
+  flex-direction: row;
+  align-items: left;
+}
+.ReactVirtualized__Table__row {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+
+  border-bottom: 2px solid $body-bg;
+}
+
+.ReactVirtualized__Table__headerTruncatedText {
+  display: inline-block;
+  max-width: 100%;
+  white-space: nowrap;
+  text-overflow: ellipsis;
+  overflow: hidden;
+}
+
+.ReactVirtualized__Table__headerColumn,
+.ReactVirtualized__Table__rowColumn {
+  margin-right: 10px;
+  min-width: 0px;
+}
+
+.ReactVirtualized__Table__headerColumn:first-of-type,
+.ReactVirtualized__Table__rowColumn:first-of-type {
+  margin-left: 10px;
+}
+.ReactVirtualized__Table__sortableHeaderColumn {
+  cursor: pointer;
+}
+
+.ReactVirtualized__Table__sortableHeaderIconContainer {
+  align-items: center;
+}
+.ReactVirtualized__Table__sortableHeaderIcon {
+  flex: 0 0 24px;
+  height: 1em;
+  width: 1em;
+  fill: currentColor;
+}
+
+.gf-table-header {
+  padding: 3px 10px;
+
+  background: $list-item-bg;
+  border-top: 2px solid $body-bg;
+  border-bottom: 2px solid $body-bg;
+
+  cursor: pointer;
+  white-space: nowrap;
+
+  color: $blue;
+}
+
+.gf-table-cell {
+  padding: 3px 10px;
+
+  background: $page-gradient;
+
+  text-overflow: ellipsis;
+  white-space: nowrap;
+
+  border-right: 2px solid $body-bg;
+  border-bottom: 2px solid $body-bg;
+}
+
+.gf-table-fixed-column {
+  border-right: 1px solid #ccc;
+}

+ 24 - 0
packages/grafana-ui/src/components/Table/_TableInputCSV.scss

@@ -0,0 +1,24 @@
+.gf-table-input-csv {
+  position: relative;
+}
+
+.gf-table-input-csv textarea {
+  height: 100%;
+  width: 100%;
+  resize: none;
+}
+
+.gf-table-input-csv footer {
+  position: absolute;
+  bottom: 15px;
+  right: 15px;
+  border: 1px solid #222;
+  background: #ccc;
+  padding: 1px 4px;
+  font-size: 80%;
+  cursor: pointer;
+}
+
+.gf-table-input-csv footer.gf-table-input-csv-err {
+  background: yellow;
+}

+ 167 - 0
packages/grafana-ui/src/components/Table/examples.ts

@@ -0,0 +1,167 @@
+import { TableData } from '../../types/data';
+import { ColumnStyle } from './TableCellBuilder';
+
+import { getColorDefinitionByName } from '@grafana/ui';
+
+const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange');
+
+export const migratedTestTable = {
+  type: 'table',
+  columns: [
+    { text: 'Time' },
+    { text: 'Value' },
+    { text: 'Colored' },
+    { text: 'Undefined' },
+    { text: 'String' },
+    { text: 'United', unit: 'bps' },
+    { text: 'Sanitized' },
+    { text: 'Link' },
+    { text: 'Array' },
+    { text: 'Mapping' },
+    { text: 'RangeMapping' },
+    { text: 'MappingColored' },
+    { text: 'RangeMappingColored' },
+  ],
+  rows: [[1388556366666, 1230, 40, undefined, '', '', 'my.host.com', 'host1', ['value1', 'value2'], 1, 2, 1, 2]],
+} as TableData;
+
+export const migratedTestStyles: ColumnStyle[] = [
+  {
+    pattern: 'Time',
+    type: 'date',
+    alias: 'Timestamp',
+  },
+  {
+    pattern: '/(Val)ue/',
+    type: 'number',
+    unit: 'ms',
+    decimals: 3,
+    alias: '$1',
+  },
+  {
+    pattern: 'Colored',
+    type: 'number',
+    unit: 'none',
+    decimals: 1,
+    colorMode: 'value',
+    thresholds: [50, 80],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+  {
+    pattern: 'String',
+    type: 'string',
+  },
+  {
+    pattern: 'String',
+    type: 'string',
+  },
+  {
+    pattern: 'United',
+    type: 'number',
+    unit: 'ms',
+    decimals: 2,
+  },
+  {
+    pattern: 'Sanitized',
+    type: 'string',
+    sanitize: true,
+  },
+  {
+    pattern: 'Link',
+    type: 'string',
+    link: true,
+    linkUrl: '/dashboard?param=$__cell&param_1=$__cell_1&param_2=$__cell_2',
+    linkTooltip: '$__cell $__cell_1 $__cell_6',
+    linkTargetBlank: true,
+  },
+  {
+    pattern: 'Array',
+    type: 'number',
+    unit: 'ms',
+    decimals: 3,
+  },
+  {
+    pattern: 'Mapping',
+    type: 'string',
+    mappingType: 1,
+    valueMaps: [
+      {
+        value: '1',
+        text: 'on',
+      },
+      {
+        value: '0',
+        text: 'off',
+      },
+      {
+        value: 'HELLO WORLD',
+        text: 'HELLO GRAFANA',
+      },
+      {
+        value: 'value1, value2',
+        text: 'value3, value4',
+      },
+    ],
+  },
+  {
+    pattern: 'RangeMapping',
+    type: 'string',
+    mappingType: 2,
+    rangeMaps: [
+      {
+        from: '1',
+        to: '3',
+        text: 'on',
+      },
+      {
+        from: '3',
+        to: '6',
+        text: 'off',
+      },
+    ],
+  },
+  {
+    pattern: 'MappingColored',
+    type: 'string',
+    mappingType: 1,
+    valueMaps: [
+      {
+        value: '1',
+        text: 'on',
+      },
+      {
+        value: '0',
+        text: 'off',
+      },
+    ],
+    colorMode: 'value',
+    thresholds: [1, 2],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+  {
+    pattern: 'RangeMappingColored',
+    type: 'string',
+    mappingType: 2,
+    rangeMaps: [
+      {
+        from: '1',
+        to: '3',
+        text: 'on',
+      },
+      {
+        from: '3',
+        to: '6',
+        text: 'off',
+      },
+    ],
+    colorMode: 'value',
+    thresholds: [2, 5],
+    colors: ['#00ff00', SemiDarkOrange.name, 'rgb(1,0,0)'],
+  },
+];
+
+export const simpleTable = {
+  type: 'table',
+  columns: [{ text: 'First' }, { text: 'Second' }, { text: 'Third' }],
+  rows: [[701, 205, 305], [702, 206, 301], [703, 207, 304]],
+};

+ 2 - 3
packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss

@@ -68,7 +68,7 @@
 }
 
 .thresholds-row-input-inner-value > input {
-  height: $gf-form-input-height;
+  height: $input-height;
   padding: $input-padding-y $input-padding-x;
   width: 150px;
   border-top: 1px solid $input-label-border-color;
@@ -86,7 +86,6 @@
 
 .thresholds-row-input-inner-color-colorpicker {
   border-radius: 10px;
-  overflow: hidden;
   display: flex;
   align-items: center;
   box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
@@ -96,7 +95,7 @@
   display: flex;
   align-items: center;
   justify-content: center;
-  height: $gf-form-input-height;
+  height: $input-height;
   padding: $input-padding-y $input-padding-x;
   width: 42px;
   background-color: $input-label-bg;

+ 77 - 0
packages/grafana-ui/src/components/VizRepeater/VizRepeater.tsx

@@ -0,0 +1,77 @@
+import React, { PureComponent } from 'react';
+import { SingleStatValueInfo, VizOrientation } from '../../types';
+
+interface RenderProps {
+  vizWidth: number;
+  vizHeight: number;
+  valueInfo: SingleStatValueInfo;
+}
+
+interface Props {
+  children: (renderProps: RenderProps) => JSX.Element | JSX.Element[];
+  height: number;
+  width: number;
+  values: SingleStatValueInfo[];
+  orientation: VizOrientation;
+}
+
+const SPACE_BETWEEN = 10;
+
+export class VizRepeater extends PureComponent<Props> {
+  getOrientation(): VizOrientation {
+    const { orientation, width, height } = this.props;
+
+    if (orientation === VizOrientation.Auto) {
+      if (width > height) {
+        return VizOrientation.Vertical;
+      } else {
+        return VizOrientation.Horizontal;
+      }
+    }
+
+    return orientation;
+  }
+
+  render() {
+    const { children, height, values, width } = this.props;
+    const orientation = this.getOrientation();
+
+    const itemStyles: React.CSSProperties = {
+      display: 'flex',
+    };
+
+    const repeaterStyle: React.CSSProperties = {
+      display: 'flex',
+    };
+
+    let vizHeight = height;
+    let vizWidth = width;
+
+    if (orientation === VizOrientation.Horizontal) {
+      repeaterStyle.flexDirection = 'column';
+      itemStyles.margin = `${SPACE_BETWEEN / 2}px 0`;
+      vizWidth = width;
+      vizHeight = height / values.length - SPACE_BETWEEN;
+    } else {
+      repeaterStyle.flexDirection = 'row';
+      itemStyles.margin = `0 ${SPACE_BETWEEN / 2}px`;
+      vizHeight = height;
+      vizWidth = width / values.length - SPACE_BETWEEN;
+    }
+
+    itemStyles.width = `${vizWidth}px`;
+    itemStyles.height = `${vizHeight}px`;
+
+    return (
+      <div style={repeaterStyle}>
+        {values.map((valueInfo, index) => {
+          return (
+            <div key={index} style={itemStyles}>
+              {children({ vizHeight, vizWidth, valueInfo })}
+            </div>
+          );
+        })}
+      </div>
+    );
+  }
+}

+ 3 - 0
packages/grafana-ui/src/components/index.scss

@@ -1,6 +1,8 @@
 @import 'CustomScrollbar/CustomScrollbar';
 @import 'DeleteButton/DeleteButton';
 @import 'ThresholdsEditor/ThresholdsEditor';
+@import 'Table/Table';
+@import 'Table/TableInputCSV';
 @import 'Tooltip/Tooltip';
 @import 'Select/Select';
 @import 'PanelOptionsGroup/PanelOptionsGroup';
@@ -9,3 +11,4 @@
 @import 'ValueMappingsEditor/ValueMappingsEditor';
 @import 'EmptySearchResult/EmptySearchResult';
 @import 'FormField/FormField';
+@import 'BarGauge/BarGauge';

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

@@ -19,12 +19,16 @@ export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
 export { ColorPicker, SeriesColorPicker } from './ColorPicker/ColorPicker';
 export { SeriesColorPickerPopover, SeriesColorPickerPopoverWithTheme } from './ColorPicker/SeriesColorPickerPopover';
 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';
 export { EmptySearchResult } from './EmptySearchResult/EmptySearchResult';
 export { PieChart, PieChartDataPoint, PieChartType } from './PieChart/PieChart';
 export { UnitPicker } from './UnitPicker/UnitPicker';
+
+// Visualizations
+export { Gauge } from './Gauge/Gauge';
+export { Graph } from './Graph/Graph';
+export { BarGauge } from './BarGauge/BarGauge';
+export { VizRepeater } from './VizRepeater/VizRepeater';

+ 23 - 23
packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts

@@ -54,34 +54,34 @@ $orange: ${theme.colors.orange};
 $purple: ${theme.colors.purple};
 $variable: ${theme.colors.variable};
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $brand-primary;
-$brand-danger: $red-base;
+$brand-primary: ${theme.colors.brandPrimary};
+$brand-success: ${theme.colors.brandSuccess};
+$brand-warning: ${theme.colors.brandWarning};
+$brand-danger: ${theme.colors.brandDanger};
 
-$query-red: $red-base;
-$query-green: #74e680;
-$query-purple: #fe85fc;
-$query-keyword: #66d9ef;
-$query-orange: $orange;
+$query-red: ${theme.colors.queryRed};
+$query-green: ${theme.colors.queryGreen};
+$query-purple: ${theme.colors.queryPurple};
+$query-orange: ${theme.colors.orange};
+$query-keyword: ${theme.colors.queryKeyword};
 
 // Status colors
 // -------------------------
-$online: $green-base;
-$warn: #f79520;
-$critical: $red-base;
+$online: ${theme.colors.online};
+$warn: ${theme.colors.warn};
+$critical: ${theme.colors.critical};
 
 // Scaffolding
 // -------------------------
 $body-bg: ${theme.colors.bodyBg};
 $page-bg: ${theme.colors.pageBg};
 
-$body-color: $gray-4;
-$text-color: $gray-4;
-$text-color-strong: $white;
-$text-color-weak: $gray-2;
-$text-color-faint: $dark-10;
-$text-color-emphasis: $gray-5;
+$body-color: ${theme.colors.body};
+$text-color: ${theme.colors.text};
+$text-color-strong: ${theme.colors.textStrong};
+$text-color-weak: ${theme.colors.textWeak};
+$text-color-faint: ${theme.colors.textFaint};
+$text-color-emphasis: ${theme.colors.textEmphasis};
 
 $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45);
 $textShadow: none;
@@ -99,14 +99,14 @@ $edit-gradient: linear-gradient(180deg, $dark-2 50%, $input-black);
 
 // Links
 // -------------------------
-$link-color: darken($white, 11%);
-$link-color-disabled: darken($link-color, 30%);
-$link-hover-color: $white;
-$external-link-color: $blue-light;
+$link-color: ${theme.colors.link};
+$link-color-disabled: ${theme.colors.linkDisabled};
+$link-hover-color: ${theme.colors.linkHover};
+$external-link-color: ${theme.colors.linkExternal};
 
 // Typography
 // -------------------------
-$headings-color: darken($white, 11%);
+$headings-color: ${theme.colors.headingColor};
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 

+ 23 - 23
packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts

@@ -46,34 +46,34 @@ $orange: ${theme.colors.orange};
 $purple: ${theme.colors.purple};
 $variable: ${theme.colors.variable};
 
-$brand-primary: $orange;
-$brand-success: $green-base;
-$brand-warning: $orange;
-$brand-danger: $red-base;
+$brand-primary: ${theme.colors.brandPrimary};
+$brand-success: ${theme.colors.brandSuccess};
+$brand-warning: ${theme.colors.brandWarning};
+$brand-danger: ${theme.colors.brandDanger};
 
-$query-red: $red-base;
-$query-green: $green-base;
-$query-purple: $purple;
-$query-orange: $orange;
-$query-keyword: $blue-base;
+$query-red: ${theme.colors.queryRed};
+$query-green: ${theme.colors.queryGreen};
+$query-purple: ${theme.colors.queryPurple};
+$query-orange: ${theme.colors.orange};
+$query-keyword: ${theme.colors.queryKeyword};
 
 // Status colors
 // -------------------------
-$online: $green-shade;
-$warn: #f79520;
-$critical: $red-shade;
+$online: ${theme.colors.online};
+$warn: ${theme.colors.warn};
+$critical: ${theme.colors.critical};
 
 // Scaffolding
 // -------------------------
 $body-bg: ${theme.colors.bodyBg};
 $page-bg: ${theme.colors.pageBg};
 
-$body-color: $gray-1;
-$text-color: $gray-1;
-$text-color-strong: $dark-1;
-$text-color-weak: $gray-2;
-$text-color-faint: $gray-4;
-$text-color-emphasis: $dark-2;
+$body-color: ${theme.colors.body};
+$text-color: ${theme.colors.text};
+$text-color-strong: ${theme.colors.textStrong};
+$text-color-weak: ${theme.colors.textWeak};
+$text-color-faint: ${theme.colors.textFaint};
+$text-color-emphasis: ${theme.colors.textEmphasis};
 
 $text-shadow-faint: none;
 
@@ -85,14 +85,14 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%);
 
 // Links
 // -------------------------
-$link-color: $gray-1;
-$link-color-disabled: lighten($link-color, 30%);
-$link-hover-color: darken($link-color, 20%);
-$external-link-color: $blue-shade;
+$link-color: ${theme.colors.link};
+$link-color-disabled: ${theme.colors.linkDisabled};
+$link-hover-color: ${theme.colors.linkHover};
+$external-link-color: ${theme.colors.linkExternal};
 
 // Typography
 // -------------------------
-$headings-color: $text-color;
+$headings-color: ${theme.colors.headingColor};
 $abbr-border-color: $gray-2 !default;
 $text-muted: $text-color-weak;
 

+ 43 - 72
packages/grafana-ui/src/themes/_variables.scss.tmpl.ts

@@ -17,7 +17,13 @@ $enable-hover-media-query: false !default;
 // Control the default styling of most Bootstrap elements by modifying these
 // variables. Mostly focused on spacing.
 
-$spacer: 1rem !default;
+$space-xxs: ${theme.spacing.xxs} !default;
+$space-xs: ${theme.spacing.xs} !default;
+$space-sm: ${theme.spacing.sm} !default;
+$space-md: ${theme.spacing.md} !default;
+$space-lg: ${theme.spacing.lg} !default;
+$space-xl: ${theme.spacing.xl} !default;
+$spacer: ${theme.spacing.d} !default;
 $spacer-x: $spacer !default;
 $spacer-y: $spacer !default;
 $spacers: (
@@ -46,7 +52,6 @@ $spacers: (
     ),
   ),
 ) !default;
-$border-width: 1px !default;
 
 // Grid breakpoints
 //
@@ -54,11 +59,11 @@ $border-width: 1px !default;
 // adapting to different screen sizes, for use in media queries.
 
 $grid-breakpoints: (
-  xs: 0,
-  sm: 544px,
-  md: 768px,
-  lg: 992px,
-  xl: 1200px,
+  xs: ${theme.breakpoints.xs},
+  sm: ${theme.breakpoints.sm},
+  md: ${theme.breakpoints.md},
+  lg: ${theme.breakpoints.lg},
+  xl: ${theme.breakpoints.xl},
 ) !default;
 
 // Grid containers
@@ -77,72 +82,51 @@ $container-max-widths: (
 // Set the number of columns and specify the width of the gutters.
 
 $grid-columns: 12 !default;
-$grid-gutter-width: 30px !default;
-
-$enable-flex: true;
+$grid-gutter-width: ${theme.spacing.gutter} !default;
 
 // Typography
 // -------------------------
 
-$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif;
-$font-family-serif: Georgia, 'Times New Roman', Times, serif;
-$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace;
-$font-family-base: $font-family-sans-serif !default;
-
-$font-size-root: 14px !default;
-$font-size-base: 13px !default;
+$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif};
+$font-family-monospace: ${theme.typography.fontFamily.monospace};
 
-$font-size-lg: 18px !default;
-$font-size-md: 14px !default;
-$font-size-sm: 12px !default;
-$font-size-xs: 10px !default;
+$font-size-root: ${theme.typography.size.root} !default;
+$font-size-base: ${theme.typography.size.base} !default;
 
-$line-height-base: 1.5 !default;
-$font-weight-semi-bold: 500;
+$font-size-lg: ${theme.typography.size.lg} !default;
+$font-size-md: ${theme.typography.size.md} !default;
+$font-size-sm: ${theme.typography.size.sm} !default;
+$font-size-xs: ${theme.typography.size.xs} !default;
 
-$font-size-h1: 2rem !default;
-$font-size-h2: 1.75rem !default;
-$font-size-h3: 1.5rem !default;
-$font-size-h4: 1.3rem !default;
-$font-size-h5: 1.2rem !default;
-$font-size-h6: 1rem !default;
+$line-height-base: ${theme.typography.lineHeight.lg} !default;
 
-$display1-size: 6rem !default;
-$display2-size: 5.5rem !default;
-$display3-size: 4.5rem !default;
-$display4-size: 3.5rem !default;
+$font-weight-regular: ${theme.typography.weight.regular} !default;
+$font-weight-semi-bold: ${theme.typography.weight.semibold} !default;
 
-$display1-weight: 400 !default;
-$display2-weight: 400 !default;
-$display3-weight: 400 !default;
-$display4-weight: 400 !default;
+$font-size-h1: ${theme.typography.heading.h1} !default;
+$font-size-h2: ${theme.typography.heading.h2} !default;
+$font-size-h3: ${theme.typography.heading.h3} !default;
+$font-size-h4: ${theme.typography.heading.h4} !default;
+$font-size-h5: ${theme.typography.heading.h5} !default;
+$font-size-h6: ${theme.typography.heading.h6} !default;
 
-$lead-font-size: 1.25rem !default;
-$lead-font-weight: 300 !default;
-
-$headings-margin-bottom: ($spacer / 2) !default;
 $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif;
-$headings-font-weight: 400 !default;
-$headings-line-height: 1.1 !default;
-
-$hr-border-width: $border-width !default;
-$dt-font-weight: bold !default;
+$headings-line-height: ${theme.typography.lineHeight.sm} !default;
 
 // Components
 //
 // Define common padding and border radius sizes and more.
 
-$line-height-lg: (4 / 3) !default;
-$line-height-sm: 1.5 !default;
+$border-width: ${theme.border.width.sm} !default;
 
-$border-radius: 3px !default;
-$border-radius-lg: 5px !default;
-$border-radius-sm: 2px !default;
+$border-radius: ${theme.border.radius.md} !default;
+$border-radius-lg: ${theme.border.radius.lg}!default;
+$border-radius-sm: ${theme.border.radius.sm} !default;
 
 // Page
 
-$page-sidebar-width: 11rem;
-$page-sidebar-margin: 4rem;
+$page-sidebar-width: 154px;
+$page-sidebar-margin: 56px;
 
 // Links
 // -------------------------
@@ -160,23 +144,17 @@ $input-padding-x: 10px !default;
 $input-padding-y: 8px !default;
 $input-line-height: 18px !default;
 
-$input-btn-border-width: 1px;
 $input-border-radius: 0 $border-radius $border-radius 0 !default;
 $input-border-radius-sm: 0 $border-radius-sm $border-radius-sm 0 !default;
 
 $label-border-radius: $border-radius 0 0 $border-radius !default;
 $label-border-radius-sm: $border-radius-sm 0 0 $border-radius-sm !default;
 
-$input-padding-y-sm: 4px !default;
-
 $input-padding-x-lg: 20px !default;
 $input-padding-y-lg: 10px !default;
 
 $input-height: 35px !default;
 
-$gf-form-margin: 0.2rem;
-$gf-form-input-height: 35px;
-
 $cursor-disabled: not-allowed !default;
 
 // Form validation icons
@@ -199,13 +177,13 @@ $zindex-typeahead: 1060;
 // Buttons
 //
 
-$btn-padding-x: 1rem !default;
-$btn-padding-y: 0.7rem !default;
+$btn-padding-x: 14px !default;
+$btn-padding-y: 10px !default;
 $btn-line-height: 1 !default;
-$btn-font-weight: 500 !default;
+$btn-font-weight: ${theme.typography.weight.semibold} !default;
 
-$btn-padding-x-sm: 0.5rem !default;
-$btn-padding-y-sm: 0.25rem !default;
+$btn-padding-x-sm: 7px !default;
+$btn-padding-y-sm: 4px !default;
 
 $btn-padding-x-lg: 21px !default;
 $btn-padding-y-lg: 11px !default;
@@ -213,7 +191,6 @@ $btn-padding-y-lg: 11px !default;
 $btn-padding-x-xl: 21px !default;
 $btn-padding-y-xl: 11px !default;
 
-$btn-border-radius: 2px;
 
 $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 
@@ -221,8 +198,7 @@ $btn-semi-transparent: rgba(0, 0, 0, 0.2) !default;
 $side-menu-width: 60px;
 
 // dashboard
-$panel-margin: 10px;
-$dashboard-padding: $panel-margin * 2;
+$dashboard-padding: 10px * 2;
 $panel-horizontal-padding: 10;
 $panel-vertical-padding: 5;
 $panel-padding: 0px $panel-horizontal-padding + 0px $panel-vertical-padding + 0px $panel-horizontal-padding + 0px;
@@ -257,9 +233,4 @@ $external-services: (
     icon: '',
   ),
 ) !default;
-
-:export {
-  panelhorizontalpadding: $panel-horizontal-padding;
-  panelverticalpadding: $panel-vertical-padding;
-}
 `;

+ 14 - 10
packages/grafana-ui/src/themes/dark.ts

@@ -46,6 +46,10 @@ const darkTheme: GrafanaTheme = {
   colors: {
     ...basicColors,
     inputBlack: '#09090b',
+    brandPrimary: basicColors.orange,
+    brandSuccess: basicColors.greenBase,
+    brandWarning: basicColors.orange,
+    brandDanger: basicColors.redBase,
     queryRed: basicColors.redBase,
     queryGreen: '#74e680',
     queryPurple: '#fe85fc',
@@ -56,16 +60,16 @@ const darkTheme: GrafanaTheme = {
     critical: basicColors.redBase,
     bodyBg: basicColors.dark2,
     pageBg: basicColors.dark2,
-    bodyColor: basicColors.gray4,
-    textColor: basicColors.gray4,
-    textColorStrong: basicColors.white,
-    textColorWeak: basicColors.gray2,
-    textColorEmphasis: basicColors.gray5,
-    textColorFaint: basicColors.dark5,
-    linkColor: new tinycolor(basicColors.white).darken(11).toString(),
-    linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(),
-    linkColorHover: basicColors.white,
-    linkColorExternal: basicColors.blue,
+    body: basicColors.gray4,
+    text: basicColors.gray4,
+    textStrong: basicColors.white,
+    textWeak: basicColors.gray2,
+    textEmphasis: basicColors.gray5,
+    textFaint: basicColors.dark5,
+    link: new tinycolor(basicColors.white).darken(11).toString(),
+    linkDisabled: new tinycolor(basicColors.white).darken(11).toString(),
+    linkHover: basicColors.white,
+    linkExternal: basicColors.blue,
     headingColor: new tinycolor(basicColors.white).darken(11).toString(),
   },
   background: {

+ 35 - 25
packages/grafana-ui/src/themes/default.ts

@@ -5,57 +5,67 @@ const theme: GrafanaThemeCommons = {
   typography: {
     fontFamily: {
       sansSerif: "'Roboto', Helvetica, Arial, sans-serif",
-      serif: "Georgia, 'Times New Roman', Times, serif",
       monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace",
     },
     size: {
+      root: '14px',
       base: '13px',
       xs: '10px',
-      s: '12px',
-      m: '14px',
-      l: '18px',
+      sm: '12px',
+      md: '14px',
+      lg: '18px',
     },
     heading: {
-      h1: '2rem',
-      h2: '1.75rem',
-      h3: '1.5rem',
-      h4: '1.3rem',
-      h5: '1.2rem',
-      h6: '1rem',
+      h1: '28px',
+      h2: '24px',
+      h3: '21px',
+      h4: '18px',
+      h5: '16px',
+      h6: '14px',
     },
     weight: {
       light: 300,
-      normal: 400,
+      regular: 400,
       semibold: 500,
     },
     lineHeight: {
       xs: 1,
-      s: 1.1,
-      m: 4 / 3,
-      l: 1.5,
+      sm: 1.1,
+      md: 4 / 3,
+      lg: 1.5,
     },
   },
-  brakpoints: {
+  breakpoints: {
     xs: '0',
-    s: '544px',
-    m: '768px',
-    l: '992px',
+    sm: '544px',
+    md: '768px',
+    lg: '992px',
     xl: '1200px',
   },
   spacing: {
-    xs: '0',
-    s: '0.2rem',
-    m: '1rem',
-    l: '1.5rem',
+    d: '14px',
+    xxs: '2px',
+    xs: '4px',
+    sm: '8px',
+    md: '16px',
+    lg: '24px',
+    xl: '32px',
     gutter: '30px',
   },
   border: {
     radius: {
-      xs: '2px',
-      s: '3px',
-      m: '5px',
+      sm: '2px',
+      md: '3px',
+      lg: '5px',
+    },
+    width: {
+      sm: '1px',
     },
   },
+  panelPadding: {
+    horizontal: 10,
+    vertical: 5,
+  },
 };
 
 export default theme;

+ 16 - 12
packages/grafana-ui/src/themes/light.ts

@@ -47,26 +47,30 @@ const lightTheme: GrafanaTheme = {
     ...basicColors,
     variable: basicColors.blue,
     inputBlack: '#09090b',
-    queryRed: basicColors.red,
+    brandPrimary: basicColors.orange,
+    brandSuccess: basicColors.greenBase,
+    brandWarning: basicColors.orange,
+    brandDanger: basicColors.redBase,
+    queryRed: basicColors.redBase,
     queryGreen: basicColors.greenBase,
     queryPurple: basicColors.purple,
-    queryKeyword: basicColors.blue,
+    queryKeyword: basicColors.blueBase,
     queryOrange: basicColors.orange,
     online: basicColors.greenShade,
     warn: '#f79520',
     critical: basicColors.redShade,
     bodyBg: basicColors.gray7,
     pageBg: basicColors.gray7,
-    bodyColor: basicColors.gray1,
-    textColor: basicColors.gray1,
-    textColorStrong: basicColors.dark2,
-    textColorWeak: basicColors.gray2,
-    textColorEmphasis: basicColors.gray5,
-    textColorFaint: basicColors.dark4,
-    linkColor: basicColors.gray1,
-    linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
-    linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(),
-    linkColorExternal: basicColors.blueLight,
+    body: basicColors.gray1,
+    text: basicColors.gray1,
+    textStrong: basicColors.dark2,
+    textWeak: basicColors.gray2,
+    textEmphasis: basicColors.gray5,
+    textFaint: basicColors.dark4,
+    link: basicColors.gray1,
+    linkDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(),
+    linkHover: new tinycolor(basicColors.gray1).darken(20).toString(),
+    linkExternal: basicColors.blueLight,
     headingColor: basicColors.gray1,
   },
   background: {

+ 11 - 5
packages/grafana-ui/src/types/data.ts

@@ -48,12 +48,9 @@ export enum NullValueMode {
 }
 
 /** View model projection of many time series */
-export interface TimeSeriesVMs {
-  [index: number]: TimeSeriesVM;
-  length: number;
-}
+export type TimeSeriesVMs = TimeSeriesVM[];
 
-interface Column {
+export interface Column {
   text: string;
   title?: string;
   type?: string;
@@ -69,3 +66,12 @@ export interface TableData {
   type: string;
   columnMap: any;
 }
+
+export type SingleStatValue = number | string | null;
+
+/*
+ * So we can add meta info like tags & series name
+ */
+export interface SingleStatValueInfo {
+  value: SingleStatValue;
+}

+ 3 - 1
packages/grafana-ui/src/types/datasource.ts

@@ -3,9 +3,11 @@ import { PluginMeta } from './plugin';
 import { TableData, TimeSeries } from './data';
 
 export interface DataQueryResponse {
-  data: TimeSeries[] | [TableData] | any;
+  data: DataQueryResponseData;
 }
 
+export type DataQueryResponseData = TimeSeries[] | [TableData] | any;
+
 export interface DataQuery {
   /**
    * A - Z

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

@@ -4,3 +4,4 @@ export * from './panel';
 export * from './plugin';
 export * from './datasource';
 export * from './theme';
+export * from './threshold';

+ 15 - 12
packages/grafana-ui/src/types/panel.ts

@@ -1,8 +1,9 @@
 import { ComponentClass } from 'react';
 import { TimeSeries, LoadingState, TableData } from './data';
 import { TimeRange } from './time';
+import { ScopedVars } from './datasource';
 
-export type InterpolateFunction = (value: string, format?: string | Function) => string;
+export type InterpolateFunction = (value: string, scopedVars?: ScopedVars, format?: string | Function) => string;
 
 export interface PanelProps<T = any> {
   panelData: PanelData;
@@ -25,10 +26,13 @@ export interface PanelEditorProps<T = any> {
   onOptionsChange: (options: T) => void;
 }
 
+export type PreservePanelOptionsHandler<TOptions = any> = (pluginId: string, prevOptions: any) => Partial<TOptions>;
+
 export class ReactPanelPlugin<TOptions = any> {
   panel: ComponentClass<PanelProps<TOptions>>;
   editor?: ComponentClass<PanelEditorProps<TOptions>>;
   defaults?: TOptions;
+  preserveOptions?: PreservePanelOptionsHandler<TOptions>;
 
   constructor(panel: ComponentClass<PanelProps<TOptions>>) {
     this.panel = panel;
@@ -41,6 +45,10 @@ export class ReactPanelPlugin<TOptions = any> {
   setDefaults(defaults: TOptions) {
     this.defaults = defaults;
   }
+
+  setPreserveOptionsHandler(handler: PreservePanelOptionsHandler<TOptions>) {
+    this.preserveOptions = handler;
+  }
 }
 
 export interface PanelSize {
@@ -57,17 +65,6 @@ export interface PanelMenuItem {
   subMenu?: PanelMenuItem[];
 }
 
-export interface Threshold {
-  index: number;
-  value: number;
-  color: string;
-}
-
-export enum BasicGaugeColor {
-  Green = '#299c46',
-  Red = '#d44a3a',
-}
-
 export enum MappingType {
   ValueToText = 1,
   RangeToText = 2,
@@ -90,3 +87,9 @@ export interface RangeMap extends BaseMap {
   from: string;
   to: string;
 }
+
+export enum VizOrientation {
+  Auto = 'auto',
+  Vertical = 'vertical',
+  Horizontal = 'horizontal',
+}

+ 46 - 28
packages/grafana-ui/src/types/theme.ts

@@ -6,36 +6,36 @@ export enum GrafanaThemeType {
 export interface GrafanaThemeCommons {
   name: string;
   // TODO: not sure if should be a part of theme
-  brakpoints: {
+  breakpoints: {
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
     xl: string;
   };
   typography: {
     fontFamily: {
       sansSerif: string;
-      serif: string;
       monospace: string;
     };
     size: {
+      root: string;
       base: string;
       xs: string;
-      s: string;
-      m: string;
-      l: string;
+      sm: string;
+      md: string;
+      lg: string;
     };
     weight: {
       light: number;
-      normal: number;
+      regular: number;
       semibold: number;
     };
     lineHeight: {
       xs: number; //1
-      s: number; //1.1
-      m: number; // 4/3
-      l: number; // 1.5
+      sm: number; //1.1
+      md: number; // 4/3
+      lg: number; // 1.5
     };
     // TODO: Refactor to use size instead of custom defs
     heading: {
@@ -48,19 +48,29 @@ export interface GrafanaThemeCommons {
     };
   };
   spacing: {
+    d: string;
+    xxs: string;
     xs: string;
-    s: string;
-    m: string;
-    l: string;
+    sm: string;
+    md: string;
+    lg: string;
+    xl: string;
     gutter: string;
   };
   border: {
     radius: {
-      xs: string;
-      s: string;
-      m: string;
+      sm: string;
+      md: string;
+      lg: string;
+    };
+    width: {
+      sm: string;
     };
   };
+  panelPadding: {
+    horizontal: number;
+    vertical: number;
+  };
 }
 
 export interface GrafanaTheme extends GrafanaThemeCommons {
@@ -113,25 +123,33 @@ export interface GrafanaTheme extends GrafanaThemeCommons {
     queryPurple: string;
     queryKeyword: string;
     queryOrange: string;
+    brandPrimary: string;
+    brandSuccess: string;
+    brandWarning: string;
+    brandDanger: string;
 
     // Status colors
     online: string;
     warn: string;
     critical: string;
 
+    // Link colors
+    link: string;
+    linkDisabled: string;
+    linkHover: string;
+    linkExternal: string;
+
+    // Text colors
+    body: string;
+    text: string;
+    textStrong: string;
+    textWeak: string;
+    textFaint: string;
+    textEmphasis: string;
+
     // TODO: move to background section
     bodyBg: string;
     pageBg: string;
-    bodyColor: string;
-    textColor: string;
-    textColorStrong: string;
-    textColorWeak: string;
-    textColorFaint: string;
-    textColorEmphasis: string;
-    linkColor: string;
-    linkColorDisabled: string;
-    linkColorHover: string;
-    linkColorExternal: string;
     headingColor: string;
   };
 }

+ 5 - 0
packages/grafana-ui/src/types/threshold.ts

@@ -0,0 +1,5 @@
+export interface Threshold {
+  index: number;
+  value: number;
+  color: string;
+}

+ 66 - 0
packages/grafana-ui/src/utils/__snapshots__/processTableData.test.ts.snap

@@ -0,0 +1,66 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`processTableData basic processing should generate a header and fix widths 1`] = `
+Object {
+  "columnMap": Object {},
+  "columns": Array [
+    Object {
+      "text": "Column 1",
+    },
+    Object {
+      "text": "Column 2",
+    },
+    Object {
+      "text": "Column 3",
+    },
+  ],
+  "rows": Array [
+    Array [
+      1,
+      null,
+      null,
+    ],
+    Array [
+      2,
+      3,
+      4,
+    ],
+    Array [
+      5,
+      6,
+      null,
+    ],
+  ],
+  "type": "table",
+}
+`;
+
+exports[`processTableData basic processing should read header and two rows 1`] = `
+Object {
+  "columnMap": Object {},
+  "columns": Array [
+    Object {
+      "text": "a",
+    },
+    Object {
+      "text": "b",
+    },
+    Object {
+      "text": "c",
+    },
+  ],
+  "rows": Array [
+    Array [
+      1,
+      2,
+      3,
+    ],
+    Array [
+      4,
+      5,
+      6,
+    ],
+  ],
+  "type": "table",
+}
+`;

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

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

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

@@ -1,5 +1,9 @@
 export * from './processTimeSeries';
+export * from './singlestat';
 export * from './valueFormats/valueFormats';
 export * from './colors';
 export * from './namedColorsPalette';
+export * from './thresholds';
+export * from './string';
+export * from './deprecationWarning';
 export { getMappedValue } from './valueMappings';

+ 20 - 0
packages/grafana-ui/src/utils/processTableData.test.ts

@@ -0,0 +1,20 @@
+import { parseCSV } from './processTableData';
+
+describe('processTableData', () => {
+  describe('basic processing', () => {
+    it('should read header and two rows', () => {
+      const text = 'a,b,c\n1,2,3\n4,5,6';
+      expect(parseCSV(text)).toMatchSnapshot();
+    });
+
+    it('should generate a header and fix widths', () => {
+      const text = '1\n2,3,4\n5,6';
+      const table = parseCSV(text, {
+        headerIsFirstLine: false,
+      });
+      expect(table.rows.length).toBe(3);
+
+      expect(table).toMatchSnapshot();
+    });
+  });
+});

+ 157 - 0
packages/grafana-ui/src/utils/processTableData.ts

@@ -0,0 +1,157 @@
+// Libraries
+import isNumber from 'lodash/isNumber';
+import Papa, { ParseError, ParseMeta } from 'papaparse';
+
+// Types
+import { TableData, Column } from '../types';
+
+// Subset of all parse options
+export interface TableParseOptions {
+  headerIsFirstLine?: boolean; // Not a papa-parse option
+  delimiter?: string; // default: ","
+  newline?: string; // default: "\r\n"
+  quoteChar?: string; // default: '"'
+  encoding?: string; // default: ""
+  comments?: boolean | string; // default: false
+}
+
+export interface TableParseDetails {
+  meta?: ParseMeta;
+  errors?: ParseError[];
+}
+
+/**
+ * This makes sure the header and all rows have equal length.
+ *
+ * @param table (immutable)
+ * @returns a new table that has equal length rows, or the same
+ * table if no changes were needed
+ */
+export function matchRowSizes(table: TableData): TableData {
+  const { rows } = table;
+  let { columns } = table;
+
+  let sameSize = true;
+  let size = columns.length;
+  rows.forEach(row => {
+    if (size !== row.length) {
+      sameSize = false;
+      size = Math.max(size, row.length);
+    }
+  });
+  if (sameSize) {
+    return table;
+  }
+
+  // Pad Columns
+  if (size !== columns.length) {
+    const diff = size - columns.length;
+    columns = [...columns];
+    for (let i = 0; i < diff; i++) {
+      columns.push({
+        text: 'Column ' + (columns.length + 1),
+      });
+    }
+  }
+
+  // Pad Rows
+  const fixedRows: any[] = [];
+  rows.forEach(row => {
+    const diff = size - row.length;
+    if (diff > 0) {
+      row = [...row];
+      for (let i = 0; i < diff; i++) {
+        row.push(null);
+      }
+    }
+    fixedRows.push(row);
+  });
+
+  return {
+    columns,
+    rows: fixedRows,
+    type: table.type,
+    columnMap: table.columnMap,
+  };
+}
+
+function makeColumns(values: any[]): Column[] {
+  return values.map((value, index) => {
+    if (!value) {
+      value = 'Column ' + (index + 1);
+    }
+    return {
+      text: value.toString().trim(),
+    };
+  });
+}
+
+/**
+ * Convert CSV text into a valid TableData object
+ *
+ * @param text
+ * @param options
+ * @param details, if exists the result will be filled with debugging details
+ */
+export function parseCSV(text: string, options?: TableParseOptions, details?: TableParseDetails): TableData {
+  const results = Papa.parse(text, { ...options, dynamicTyping: true, skipEmptyLines: true });
+  const { data, meta, errors } = results;
+
+  // Fill the parse details for debugging
+  if (details) {
+    details.errors = errors;
+    details.meta = meta;
+  }
+
+  if (!data || data.length < 1) {
+    // Show a more reasonable warning on empty input text
+    if (details && !text) {
+      errors.length = 0;
+      errors.push({
+        code: 'empty',
+        message: 'Empty input text',
+        type: 'warning',
+        row: 0,
+      });
+      details.errors = errors;
+    }
+    return {
+      columns: [],
+      rows: [],
+      type: 'table',
+      columnMap: {},
+    };
+  }
+
+  // Assume the first line is the header unless the config says its not
+  const headerIsNotFirstLine = options && options.headerIsFirstLine === false;
+  const header = headerIsNotFirstLine ? [] : results.data.shift();
+
+  return matchRowSizes({
+    columns: makeColumns(header),
+    rows: results.data,
+    type: 'table',
+    columnMap: {},
+  });
+}
+
+export function sortTableData(data: TableData, sortIndex?: number, reverse = false): TableData {
+  if (isNumber(sortIndex)) {
+    const copy = {
+      ...data,
+      rows: [...data.rows].sort((a, b) => {
+        a = a[sortIndex];
+        b = b[sortIndex];
+        // Sort null or undefined separately from comparable values
+        return +(a == null) - +(b == null) || +(a > b) || -(a < b);
+      }),
+    };
+
+    if (reverse) {
+      copy.rows.reverse();
+    }
+
+    return copy;
+  }
+  return data;
+}

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

@@ -1,6 +0,0 @@
-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;

+ 33 - 0
packages/grafana-ui/src/utils/singlestat.ts

@@ -0,0 +1,33 @@
+import { PanelData, NullValueMode, SingleStatValueInfo } from '../types';
+import { processTimeSeries } from './processTimeSeries';
+
+export interface SingleStatProcessingOptions {
+  panelData: PanelData;
+  stat: string;
+}
+
+//
+// This is a temporary thing, waiting for a better data model and maybe unification between time series & table data
+//
+export function processSingleStatPanelData(options: SingleStatProcessingOptions): SingleStatValueInfo[] {
+  const { panelData, stat } = options;
+
+  if (panelData.timeSeries) {
+    const timeSeries = processTimeSeries({
+      timeSeries: panelData.timeSeries,
+      nullValueMode: NullValueMode.Null,
+    });
+
+    return timeSeries.map((series, index) => {
+      const value = stat !== 'name' ? series.stats[stat] : series.label;
+
+      return {
+        value: value,
+      };
+    });
+  } else if (panelData.tableData) {
+    throw { message: 'Panel data not supported' };
+  }
+
+  return [];
+}

+ 24 - 0
packages/grafana-ui/src/utils/storybook/withFullSizeStory.tsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import { AutoSizer } from 'react-virtualized';
+
+/** This will add full size with & height properties */
+export const withFullSizeStory = (component: React.ComponentType<any>, props: any) => (
+  <div
+    style={{
+      height: '100vh',
+      width: '100%',
+    }}
+  >
+    <AutoSizer>
+      {({ width, height }) => (
+        <>
+          {React.createElement(component, {
+            ...props,
+            width,
+            height,
+          })}
+        </>
+      )}
+    </AutoSizer>
+  </div>
+);

+ 15 - 0
packages/grafana-ui/src/utils/string.test.ts

@@ -0,0 +1,15 @@
+import { stringToJsRegex } from '@grafana/ui';
+
+describe('stringToJsRegex', () => {
+  it('should parse the valid regex value', () => {
+    const output = stringToJsRegex('/validRegexp/');
+    expect(output).toBeInstanceOf(RegExp);
+  });
+
+  it('should throw error on invalid regex value', () => {
+    const input = '/etc/hostname';
+    expect(() => {
+      stringToJsRegex(input);
+    }).toThrow();
+  });
+});

+ 13 - 0
packages/grafana-ui/src/utils/string.ts

@@ -0,0 +1,13 @@
+export function stringToJsRegex(str: string): RegExp {
+  if (str[0] !== '/') {
+    return new RegExp('^' + str + '$');
+  }
+
+  const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+
+  if (!match) {
+    throw new Error(`'${str}' is not a valid regular expression.`);
+  }
+
+  return new RegExp(match[1], match[2]);
+}

+ 23 - 0
packages/grafana-ui/src/utils/thresholds.ts

@@ -0,0 +1,23 @@
+import { Threshold } from '../types';
+
+export function getThresholdForValue(
+  thresholds: Threshold[],
+  value: number | null | string | undefined
+): Threshold | null {
+  if (thresholds.length === 1) {
+    return thresholds[0];
+  }
+
+  const atThreshold = thresholds.filter(threshold => (value as number) === threshold.value)[0];
+  if (atThreshold) {
+    return atThreshold;
+  }
+
+  const belowThreshold = thresholds.filter(threshold => (value as number) > threshold.value);
+  if (belowThreshold.length > 0) {
+    const nearestThreshold = belowThreshold.sort((t1: Threshold, t2: Threshold) => t2.value - t1.value)[0];
+    return nearestThreshold;
+  }
+
+  return null;
+}

+ 1 - 1
packages/grafana-ui/src/utils/valueFormats/categories.ts

@@ -137,7 +137,7 @@ export const getCategories = (): ValueFormatCategory[] => [
     formats: [
       { name: 'packets/sec', id: 'pps', fn: decimalSIPrefix('pps') },
       { name: 'bits/sec', id: 'bps', fn: decimalSIPrefix('bps') },
-      { name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('B/s') },
+      { name: 'bytes/sec', id: 'Bps', fn: decimalSIPrefix('Bs') },
       { name: 'kilobytes/sec', id: 'KBs', fn: decimalSIPrefix('Bs', 1) },
       { name: 'kilobits/sec', id: 'Kbits', fn: decimalSIPrefix('bps', 1) },
       { name: 'megabytes/sec', id: 'MBs', fn: decimalSIPrefix('Bs', 2) },

+ 2 - 1
packaging/docker/Dockerfile

@@ -9,7 +9,8 @@ RUN apt-get update && apt-get install -qq -y tar && \
 
 COPY ${GRAFANA_TGZ} /tmp/grafana.tar.gz
 
-RUN mkdir /tmp/grafana && tar xfvz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
+# Change to tar xfzv to make tar print every file it extracts
+RUN mkdir /tmp/grafana && tar xfz /tmp/grafana.tar.gz --strip-components=1 -C /tmp/grafana
 
 ARG BASE_IMAGE=debian:stretch-slim
 FROM ${BASE_IMAGE}

+ 1 - 0
pkg/api/dashboard.go

@@ -488,6 +488,7 @@ func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.Resto
 	saveCmd.Dashboard.Set("version", dash.Version)
 	saveCmd.Dashboard.Set("uid", dash.Uid)
 	saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
+	saveCmd.FolderId = dash.FolderId
 
 	return hs.PostDashboard(c, saveCmd)
 }

+ 124 - 0
pkg/api/dashboard_test.go

@@ -810,6 +810,93 @@ func TestDashboardApiEndpoint(t *testing.T) {
 			})
 		})
 	})
+
+	Convey("Given dashboard in folder being restored should restore to folder", t, func() {
+		fakeDash := m.NewDashboard("Child dash")
+		fakeDash.Id = 2
+		fakeDash.FolderId = 1
+		fakeDash.HasAcl = false
+
+		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+			query.Result = fakeDash
+			return nil
+		})
+
+		bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
+			query.Result = &m.DashboardVersion{
+				DashboardId: 2,
+				Version:     1,
+				Data:        fakeDash.Data,
+			}
+			return nil
+		})
+
+		mock := &dashboards.FakeDashboardService{
+			SaveDashboardResult: &m.Dashboard{
+				Id:      2,
+				Uid:     "uid",
+				Title:   "Dash",
+				Slug:    "dash",
+				Version: 1,
+			},
+		}
+
+		cmd := dtos.RestoreDashboardVersionCommand{
+			Version: 1,
+		}
+
+		restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) {
+			CallRestoreDashboardVersion(sc)
+			So(sc.resp.Code, ShouldEqual, 200)
+			dto := mock.SavedDashboards[0]
+			So(dto.Dashboard.FolderId, ShouldEqual, 1)
+			So(dto.Dashboard.Title, ShouldEqual, "Child dash")
+			So(dto.Message, ShouldEqual, "Restored from version 1")
+		})
+	})
+
+	Convey("Given dashboard in general folder being restored should restore to general folder", t, func() {
+		fakeDash := m.NewDashboard("Child dash")
+		fakeDash.Id = 2
+		fakeDash.HasAcl = false
+
+		bus.AddHandler("test", func(query *m.GetDashboardQuery) error {
+			query.Result = fakeDash
+			return nil
+		})
+
+		bus.AddHandler("test", func(query *m.GetDashboardVersionQuery) error {
+			query.Result = &m.DashboardVersion{
+				DashboardId: 2,
+				Version:     1,
+				Data:        fakeDash.Data,
+			}
+			return nil
+		})
+
+		mock := &dashboards.FakeDashboardService{
+			SaveDashboardResult: &m.Dashboard{
+				Id:      2,
+				Uid:     "uid",
+				Title:   "Dash",
+				Slug:    "dash",
+				Version: 1,
+			},
+		}
+
+		cmd := dtos.RestoreDashboardVersionCommand{
+			Version: 1,
+		}
+
+		restoreDashboardVersionScenario("When calling POST on", "/api/dashboards/id/1/restore", "/api/dashboards/id/:dashboardId/restore", mock, cmd, func(sc *scenarioContext) {
+			CallRestoreDashboardVersion(sc)
+			So(sc.resp.Code, ShouldEqual, 200)
+			dto := mock.SavedDashboards[0]
+			So(dto.Dashboard.FolderId, ShouldEqual, 0)
+			So(dto.Dashboard.Title, ShouldEqual, "Child dash")
+			So(dto.Message, ShouldEqual, "Restored from version 1")
+		})
+	})
 }
 
 func GetDashboardShouldReturn200(sc *scenarioContext) dtos.DashboardFullWithMeta {
@@ -871,6 +958,10 @@ func CallPostDashboard(sc *scenarioContext) {
 	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
 }
 
+func CallRestoreDashboardVersion(sc *scenarioContext) {
+	sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
+}
+
 func CallPostDashboardShouldReturnSuccess(sc *scenarioContext) {
 	CallPostDashboard(sc)
 
@@ -928,6 +1019,39 @@ func postDiffScenario(desc string, url string, routePattern string, cmd dtos.Cal
 	})
 }
 
+func restoreDashboardVersionScenario(desc string, url string, routePattern string, mock *dashboards.FakeDashboardService, cmd dtos.RestoreDashboardVersionCommand, fn scenarioFunc) {
+	Convey(desc+" "+url, func() {
+		defer bus.ClearBusHandlers()
+
+		hs := HTTPServer{
+			Bus: bus.GetBus(),
+		}
+
+		sc := setupScenarioContext(url)
+		sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
+			sc.context = c
+			sc.context.SignedInUser = &m.SignedInUser{
+				OrgId:  TestOrgID,
+				UserId: TestUserID,
+			}
+			sc.context.OrgRole = m.ROLE_ADMIN
+
+			return hs.RestoreDashboardVersion(c, cmd)
+		})
+
+		origNewDashboardService := dashboards.NewService
+		dashboards.MockDashboardService(mock)
+
+		sc.m.Post(routePattern, sc.defaultHandler)
+
+		defer func() {
+			dashboards.NewService = origNewDashboardService
+		}()
+
+		fn(sc)
+	})
+}
+
 func (sc *scenarioContext) ToJSON() *simplejson.Json {
 	var result *simplejson.Json
 	err := json.NewDecoder(sc.resp.Body).Decode(&result)

+ 7 - 5
pkg/api/frontendsettings.go

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

+ 1 - 0
pkg/api/login.go

@@ -36,6 +36,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) {
 	viewData.Settings["oauth"] = enabledOAuths
 	viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp
 	viewData.Settings["loginHint"] = setting.LoginHint
+	viewData.Settings["passwordHint"] = setting.PasswordHint
 	viewData.Settings["disableLoginForm"] = setting.DisableLoginForm
 
 	if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok {

+ 9 - 2
pkg/log/log.go

@@ -25,6 +25,7 @@ var filters map[string]log15.Lvl
 func init() {
 	loggersToClose = make([]DisposableHandler, 0)
 	loggersToReload = make([]ReloadableHandler, 0)
+	filters = map[string]log15.Lvl{}
 	Root = log15.Root()
 	Root.SetHandler(log15.DiscardHandler())
 }
@@ -197,7 +198,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
 
 		// Log level.
 		_, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg)
-		filters := getFilters(util.SplitString(sec.Key("filters").String()))
+		modeFilters := getFilters(util.SplitString(sec.Key("filters").String()))
 		format := getLogFormat(sec.Key("format").MustString(""))
 
 		var handler log15.Handler
@@ -230,12 +231,18 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) {
 		}
 
 		for key, value := range defaultFilters {
+			if _, exist := modeFilters[key]; !exist {
+				modeFilters[key] = value
+			}
+		}
+
+		for key, value := range modeFilters {
 			if _, exist := filters[key]; !exist {
 				filters[key] = value
 			}
 		}
 
-		handler = LogFilterHandler(level, filters, handler)
+		handler = LogFilterHandler(level, modeFilters, handler)
 		handlers = append(handlers, handler)
 	}
 

+ 23 - 2
pkg/login/ldap.go

@@ -18,6 +18,7 @@ import (
 
 type ILdapConn interface {
 	Bind(username, password string) error
+	UnauthenticatedBind(username string) error
 	Search(*ldap.SearchRequest) (*ldap.SearchResult, error)
 	StartTLS(*tls.Config) error
 	Close()
@@ -218,8 +219,18 @@ func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo
 }
 
 func (a *ldapAuther) serverBind() error {
+	bindFn := func() error {
+		return a.conn.Bind(a.server.BindDN, a.server.BindPassword)
+	}
+
+	if a.server.BindPassword == "" {
+		bindFn = func() error {
+			return a.conn.UnauthenticatedBind(a.server.BindDN)
+		}
+	}
+
 	// bind_dn and bind_password to bind
-	if err := a.conn.Bind(a.server.BindDN, a.server.BindPassword); err != nil {
+	if err := bindFn(); err != nil {
 		a.log.Info("LDAP initial bind failed, %v", err)
 
 		if ldapErr, ok := err.(*ldap.Error); ok {
@@ -259,7 +270,17 @@ func (a *ldapAuther) initialBind(username, userPassword string) error {
 		bindPath = fmt.Sprintf(a.server.BindDN, username)
 	}
 
-	if err := a.conn.Bind(bindPath, userPassword); err != nil {
+	bindFn := func() error {
+		return a.conn.Bind(bindPath, userPassword)
+	}
+
+	if userPassword == "" {
+		bindFn = func() error {
+			return a.conn.UnauthenticatedBind(bindPath)
+		}
+	}
+
+	if err := bindFn(); err != nil {
 		a.log.Info("Initial bind failed", "error", err)
 
 		if ldapErr, ok := err.(*ldap.Error); ok {

+ 144 - 3
pkg/login/ldap_test.go

@@ -13,6 +13,133 @@ import (
 )
 
 func TestLdapAuther(t *testing.T) {
+	Convey("initialBind", t, func() {
+		Convey("Given bind dn and password configured", func() {
+			conn := &mockLdapConn{}
+			var actualUsername, actualPassword string
+			conn.bindProvider = func(username, password string) error {
+				actualUsername = username
+				actualPassword = password
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn: conn,
+				server: &LdapServerConf{
+					BindDN:       "cn=%s,o=users,dc=grafana,dc=org",
+					BindPassword: "bindpwd",
+				},
+			}
+			err := ldapAuther.initialBind("user", "pwd")
+			So(err, ShouldBeNil)
+			So(ldapAuther.requireSecondBind, ShouldBeTrue)
+			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
+			So(actualPassword, ShouldEqual, "bindpwd")
+		})
+
+		Convey("Given bind dn configured", func() {
+			conn := &mockLdapConn{}
+			var actualUsername, actualPassword string
+			conn.bindProvider = func(username, password string) error {
+				actualUsername = username
+				actualPassword = password
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn: conn,
+				server: &LdapServerConf{
+					BindDN: "cn=%s,o=users,dc=grafana,dc=org",
+				},
+			}
+			err := ldapAuther.initialBind("user", "pwd")
+			So(err, ShouldBeNil)
+			So(ldapAuther.requireSecondBind, ShouldBeFalse)
+			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
+			So(actualPassword, ShouldEqual, "pwd")
+		})
+
+		Convey("Given empty bind dn and password", func() {
+			conn := &mockLdapConn{}
+			unauthenticatedBindWasCalled := false
+			var actualUsername string
+			conn.unauthenticatedBindProvider = func(username string) error {
+				unauthenticatedBindWasCalled = true
+				actualUsername = username
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn:   conn,
+				server: &LdapServerConf{},
+			}
+			err := ldapAuther.initialBind("user", "pwd")
+			So(err, ShouldBeNil)
+			So(ldapAuther.requireSecondBind, ShouldBeTrue)
+			So(unauthenticatedBindWasCalled, ShouldBeTrue)
+			So(actualUsername, ShouldBeEmpty)
+		})
+	})
+
+	Convey("serverBind", t, func() {
+		Convey("Given bind dn and password configured", func() {
+			conn := &mockLdapConn{}
+			var actualUsername, actualPassword string
+			conn.bindProvider = func(username, password string) error {
+				actualUsername = username
+				actualPassword = password
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn: conn,
+				server: &LdapServerConf{
+					BindDN:       "o=users,dc=grafana,dc=org",
+					BindPassword: "bindpwd",
+				},
+			}
+			err := ldapAuther.serverBind()
+			So(err, ShouldBeNil)
+			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+			So(actualPassword, ShouldEqual, "bindpwd")
+		})
+
+		Convey("Given bind dn configured", func() {
+			conn := &mockLdapConn{}
+			unauthenticatedBindWasCalled := false
+			var actualUsername string
+			conn.unauthenticatedBindProvider = func(username string) error {
+				unauthenticatedBindWasCalled = true
+				actualUsername = username
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn: conn,
+				server: &LdapServerConf{
+					BindDN: "o=users,dc=grafana,dc=org",
+				},
+			}
+			err := ldapAuther.serverBind()
+			So(err, ShouldBeNil)
+			So(unauthenticatedBindWasCalled, ShouldBeTrue)
+			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
+		})
+
+		Convey("Given empty bind dn and password", func() {
+			conn := &mockLdapConn{}
+			unauthenticatedBindWasCalled := false
+			var actualUsername string
+			conn.unauthenticatedBindProvider = func(username string) error {
+				unauthenticatedBindWasCalled = true
+				actualUsername = username
+				return nil
+			}
+			ldapAuther := &ldapAuther{
+				conn:   conn,
+				server: &LdapServerConf{},
+			}
+			err := ldapAuther.serverBind()
+			So(err, ShouldBeNil)
+			So(unauthenticatedBindWasCalled, ShouldBeTrue)
+			So(actualUsername, ShouldBeEmpty)
+		})
+	})
 
 	Convey("When translating ldap user to grafana user", t, func() {
 
@@ -365,12 +492,26 @@ func TestLdapAuther(t *testing.T) {
 }
 
 type mockLdapConn struct {
-	result           *ldap.SearchResult
-	searchCalled     bool
-	searchAttributes []string
+	result                      *ldap.SearchResult
+	searchCalled                bool
+	searchAttributes            []string
+	bindProvider                func(username, password string) error
+	unauthenticatedBindProvider func(username string) error
 }
 
 func (c *mockLdapConn) Bind(username, password string) error {
+	if c.bindProvider != nil {
+		return c.bindProvider(username, password)
+	}
+
+	return nil
+}
+
+func (c *mockLdapConn) UnauthenticatedBind(username string) error {
+	if c.unauthenticatedBindProvider != nil {
+		return c.unauthenticatedBindProvider(username)
+	}
+
 	return nil
 }
 

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

@@ -138,7 +138,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 		return err
 	}
 
-	renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId)
+	renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, context.Rule.OrgId, context.Rule.PanelId)
 
 	result, err := n.renderService.Render(context.Ctx, renderOpts)
 	if err != nil {

+ 68 - 19
pkg/services/alerting/notifiers/dingding.go

@@ -1,6 +1,10 @@
 package notifiers
 
 import (
+	"fmt"
+	"net/url"
+	"strings"
+
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/log"
@@ -8,19 +12,26 @@ import (
 	"github.com/grafana/grafana/pkg/services/alerting"
 )
 
-func init() {
-	alerting.RegisterNotifier(&alerting.NotifierPlugin{
-		Type:        "dingding",
-		Name:        "DingDing",
-		Description: "Sends HTTP POST request to DingDing",
-		Factory:     NewDingDingNotifier,
-		OptionsTemplate: `
+const DefaultDingdingMsgType = "link"
+const DingdingOptionsTemplate = `
       <h3 class="page-heading">DingDing settings</h3>
       <div class="gf-form">
         <span class="gf-form-label width-10">Url</span>
-        <input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
+        <input type="text" required class="gf-form-input max-width-70" ng-model="ctrl.model.settings.url" placeholder="https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx"></input>
+      </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-10">MessageType</span>
+        <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.msgType" ng-options="s for s in ['link','actionCard']" ng-init="ctrl.model.settings.msgType=ctrl.model.settings.msgType || '` + DefaultDingdingMsgType + `'"></select>
       </div>
-    `,
+`
+
+func init() {
+	alerting.RegisterNotifier(&alerting.NotifierPlugin{
+		Type:            "dingding",
+		Name:            "DingDing",
+		Description:     "Sends HTTP POST request to DingDing",
+		Factory:         NewDingDingNotifier,
+		OptionsTemplate: DingdingOptionsTemplate,
 	})
 
 }
@@ -31,8 +42,11 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
 		return nil, alerting.ValidationError{Reason: "Could not find url property in settings"}
 	}
 
+	msgType := model.Settings.Get("msgType").MustString(DefaultDingdingMsgType)
+
 	return &DingDingNotifier{
 		NotifierBase: NewNotifierBase(model),
+		MsgType:      msgType,
 		Url:          url,
 		log:          log.New("alerting.notifier.dingding"),
 	}, nil
@@ -40,8 +54,9 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
 
 type DingDingNotifier struct {
 	NotifierBase
-	Url string
-	log log.Logger
+	MsgType string
+	Url     string
+	log     log.Logger
 }
 
 func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
@@ -52,6 +67,16 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
 		this.log.Error("Failed to get messageUrl", "error", err, "dingding", this.Name)
 		messageUrl = ""
 	}
+
+	q := url.Values{
+		"pc_slide": {"false"},
+		"url":      {messageUrl},
+	}
+
+	// Use special link to auto open the message url outside of Dingding
+	// Refer: https://open-doc.dingtalk.com/docs/doc.htm?treeId=385&articleId=104972&docType=1#s9
+	messageUrl = "dingtalk://dingtalkclient/page/link?" + q.Encode()
+
 	this.log.Info("messageUrl:" + messageUrl)
 
 	message := evalContext.Rule.Message
@@ -61,15 +86,39 @@ func (this *DingDingNotifier) Notify(evalContext *alerting.EvalContext) error {
 		message = title
 	}
 
-	bodyJSON, err := simplejson.NewJson([]byte(`{
-		"msgtype": "link",
-		"link": {
-			"text": "` + message + `",
-			"title": "` + title + `",
-			"picUrl": "` + picUrl + `",
-			"messageUrl": "` + messageUrl + `"
+	for i, match := range evalContext.EvalMatches {
+		message += fmt.Sprintf("\\n%2d. %s: %s", i+1, match.Metric, match.Value)
+	}
+
+	var bodyStr string
+	if this.MsgType == "actionCard" {
+		// Embed the pic into the markdown directly because actionCard doesn't have a picUrl field
+		if picUrl != "" {
+			message = "![](" + picUrl + ")\\n\\n" + message
 		}
-	}`))
+
+		bodyStr = `{
+			"msgtype": "actionCard",
+			"actionCard": {
+				"text": "` + strings.Replace(message, `"`, "'", -1) + `",
+				"title": "` + strings.Replace(title, `"`, "'", -1) + `",
+				"singleTitle": "More",
+				"singleURL": "` + messageUrl + `"
+			}
+		}`
+	} else {
+		bodyStr = `{
+			"msgtype": "link",
+			"link": {
+				"text": "` + message + `",
+				"title": "` + title + `",
+				"picUrl": "` + picUrl + `",
+				"messageUrl": "` + messageUrl + `"
+			}
+		}`
+	}
+
+	bodyJSON, err := simplejson.NewJson([]byte(bodyStr))
 
 	if err != nil {
 		this.log.Error("Failed to create Json data", "error", err, "dingding", this.Name)

+ 43 - 38
pkg/services/alerting/notifiers/discord.go

@@ -111,63 +111,68 @@ func (this *DiscordNotifier) Notify(evalContext *alerting.EvalContext) error {
 
 	json, _ := bodyJSON.MarshalJSON()
 
-	content_type := "application/json"
-
-	var body []byte
-
-	if embeddedImage {
-
-		var b bytes.Buffer
-
-		w := multipart.NewWriter(&b)
-
-		f, err := os.Open(evalContext.ImageOnDiskPath)
+	cmd := &m.SendWebhookSync{
+		Url:         this.WebhookURL,
+		HttpMethod:  "POST",
+		ContentType: "application/json",
+	}
 
+	if !embeddedImage {
+		cmd.Body = string(json)
+	} else {
+		err := this.embedImage(cmd, evalContext.ImageOnDiskPath, json)
 		if err != nil {
-			this.log.Error("Can't open graph file", err)
+			this.log.Error("failed to embed image", "error", err)
 			return err
 		}
+	}
 
-		defer f.Close()
-
-		fw, err := w.CreateFormField("payload_json")
-		if err != nil {
-			return err
-		}
+	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
+		this.log.Error("Failed to send notification to Discord", "error", err)
+		return err
+	}
 
-		if _, err = fw.Write([]byte(string(json))); err != nil {
-			return err
-		}
+	return nil
+}
 
-		fw, err = w.CreateFormFile("file", "graph.png")
-		if err != nil {
-			return err
+func (this *DiscordNotifier) embedImage(cmd *m.SendWebhookSync, imagePath string, existingJSONBody []byte) error {
+	f, err := os.Open(imagePath)
+	defer f.Close()
+	if err != nil {
+		if os.IsNotExist(err) {
+			cmd.Body = string(existingJSONBody)
+			return nil
 		}
-
-		if _, err = io.Copy(fw, f); err != nil {
+		if !os.IsNotExist(err) {
 			return err
 		}
+	}
 
-		w.Close()
+	var b bytes.Buffer
+	w := multipart.NewWriter(&b)
 
-		body = b.Bytes()
-		content_type = w.FormDataContentType()
+	fw, err := w.CreateFormField("payload_json")
+	if err != nil {
+		return err
+	}
 
-	} else {
-		body = json
+	if _, err = fw.Write([]byte(string(existingJSONBody))); err != nil {
+		return err
 	}
 
-	cmd := &m.SendWebhookSync{
-		Url:         this.WebhookURL,
-		Body:        string(body),
-		HttpMethod:  "POST",
-		ContentType: content_type,
+	fw, err = w.CreateFormFile("file", "graph.png")
+	if err != nil {
+		return err
 	}
 
-	if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
-		this.log.Error("Failed to send notification to Discord", "error", err)
+	if _, err = io.Copy(fw, f); err != nil {
 		return err
 	}
 
+	w.Close()
+
+	cmd.Body = string(b.Bytes())
+	cmd.ContentType = w.FormDataContentType()
+
 	return nil
 }

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

@@ -56,7 +56,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext {
 
 	ctx := NewEvalContext(context.Background(), testRule)
 	if cmd.Settings.Get("uploadImage").MustBool(true) {
-		ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png"
+		ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png"
 	}
 	ctx.IsTestRun = true
 	ctx.Firing = true

+ 15 - 4
pkg/services/rendering/phantomjs.go

@@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
 	defer middleware.RemoveRenderAuthKey(renderKey)
 
 	phantomDebugArg := "--debug=false"
-	if log.GetLogLevelFor("renderer") >= log.LvlDebug {
+	if log.GetLogLevelFor("rendering") >= log.LvlDebug {
 		phantomDebugArg = "--debug=true"
 	}
 
@@ -64,13 +64,26 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
 	cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
 	cmd.Stderr = cmd.Stdout
 
+	timezone := ""
+
 	if opts.Timezone != "" {
+		timezone = isoTimeOffsetToPosixTz(opts.Timezone)
 		baseEnviron := os.Environ()
-		cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(opts.Timezone))
+		cmd.Env = appendEnviron(baseEnviron, "TZ", timezone)
 	}
 
+	rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone)
+
 	out, err := cmd.Output()
 
+	if out != nil {
+		rs.log.Debug("Phantomjs output", "out", string(out))
+	}
+
+	if err != nil {
+		rs.log.Debug("Phantomjs error", "error", err)
+	}
+
 	// check for timeout first
 	if commandCtx.Err() == context.DeadlineExceeded {
 		rs.log.Info("Rendering timed out")
@@ -82,8 +95,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
 		return nil, err
 	}
 
-	rs.log.Debug("Phantomjs output", "out", string(out))
-
 	rs.log.Debug("Image rendered", "path", pngPath)
 	return &RenderResult{FilePath: pngPath}, nil
 }

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

@@ -309,7 +309,9 @@ func PauseAlert(cmd *m.PauseAlertCommand) error {
 			params = append(params, v)
 		}
 
-		res, err := sess.Exec(buffer.String(), params...)
+		sqlOrArgs := append([]interface{}{buffer.String()}, params...)
+
+		res, err := sess.Exec(sqlOrArgs...)
 		if err != nil {
 			return err
 		}

+ 6 - 2
pkg/services/sqlstore/annotation.go

@@ -258,11 +258,15 @@ func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
 			queryParams = []interface{}{params.DashboardId, params.PanelId, params.OrgId}
 		}
 
-		if _, err := sess.Exec(annoTagSql, queryParams...); err != nil {
+		sqlOrArgs := append([]interface{}{annoTagSql}, queryParams...)
+
+		if _, err := sess.Exec(sqlOrArgs...); err != nil {
 			return err
 		}
 
-		if _, err := sess.Exec(sql, queryParams...); err != nil {
+		sqlOrArgs = append([]interface{}{sql}, queryParams...)
+
+		if _, err := sess.Exec(sqlOrArgs...); err != nil {
 			return err
 		}
 

+ 3 - 2
pkg/services/sqlstore/dashboard_version.go

@@ -51,7 +51,7 @@ func GetDashboardVersions(query *m.GetDashboardVersionsQuery) error {
 				dashboard_version.message,
 				dashboard_version.data,`+
 			dialect.Quote("user")+`.login as created_by`).
-		Join("LEFT", "user", `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
+		Join("LEFT", dialect.Quote("user"), `dashboard_version.created_by = `+dialect.Quote("user")+`.id`).
 		Join("LEFT", "dashboard", `dashboard.id = dashboard_version.dashboard_id`).
 		Where("dashboard_version.dashboard_id=? AND dashboard.org_id=?", query.DashboardId, query.OrgId).
 		OrderBy("dashboard_version.version DESC").
@@ -102,7 +102,8 @@ func DeleteExpiredVersions(cmd *m.DeleteExpiredVersionsCommand) error {
 
 		if len(versionIdsToDelete) > 0 {
 			deleteExpiredSql := `DELETE FROM dashboard_version WHERE id IN (?` + strings.Repeat(",?", len(versionIdsToDelete)-1) + `)`
-			expiredResponse, err := sess.Exec(deleteExpiredSql, versionIdsToDelete...)
+			sqlOrArgs := append([]interface{}{deleteExpiredSql}, versionIdsToDelete...)
+			expiredResponse, err := sess.Exec(sqlOrArgs...)
 			if err != nil {
 				return err
 			}

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

@@ -174,6 +174,11 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			Version:           cmd.Version + 1,
 		}
 
+		sess.UseBool("is_default")
+		sess.UseBool("basic_auth")
+		sess.UseBool("with_credentials")
+		sess.UseBool("read_only")
+
 		var updateSession *xorm.Session
 		if cmd.Version != 0 {
 			// the reason we allow cmd.version > db.version is make it possible for people to force
@@ -185,7 +190,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error {
 			updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId)
 		}
 
-		affected, err := updateSession.AllCols().Omit("created").Update(ds)
+		affected, err := updateSession.Update(ds)
 		if err != nil {
 			return err
 		}

+ 4 - 0
pkg/services/sqlstore/login_attempt.go

@@ -44,6 +44,10 @@ func DeleteOldLoginAttempts(cmd *m.DeleteOldLoginAttemptsCommand) error {
 			return err
 		}
 
+		if result == nil || len(result) == 0 || result[0] == nil {
+			return nil
+		}
+
 		maxId = toInt64(result[0]["id"])
 
 		if maxId == 0 {

Some files were not shown because too many files changed in this diff