Browse Source

dashfolders: merge conflict

Daniel Lee 8 years ago
parent
commit
bc5fae5367
100 changed files with 3292 additions and 2002 deletions
  1. 0 1
      .editorconfig
  2. 7 0
      CHANGELOG.md
  3. 630 0
      Gopkg.lock
  4. 195 0
      Gopkg.toml
  5. 4 10
      README.md
  6. 6 6
      build.go
  7. 4 1
      conf/defaults.ini
  8. 4 1
      conf/sample.ini
  9. 7 0
      docker/blocks/mysql/docker-compose.yaml
  10. 3 0
      docker/blocks/mysql_tests/docker-compose.yaml
  11. 7 0
      docker/blocks/postgres/docker-compose.yaml
  12. 4 2
      docs/sources/alerting/notifications.md
  13. 17 0
      docs/sources/installation/behind_proxy.md
  14. 65 1
      docs/sources/installation/configuration.md
  15. 1 1
      docs/sources/project/building_from_source.md
  16. 4 4
      package.json
  17. 4 0
      pkg/api/http_server.go
  18. 12 9
      pkg/api/login_oauth.go
  19. 2 0
      pkg/components/imguploader/imguploader.go
  20. 18 0
      pkg/components/imguploader/imguploader_test.go
  21. 22 0
      pkg/components/imguploader/localuploader.go
  22. 18 0
      pkg/components/imguploader/localuploader_test.go
  23. 2 1
      pkg/metrics/graphitebridge/graphite.go
  24. 1 0
      pkg/metrics/metrics.go
  25. 1 0
      pkg/models/stats.go
  26. 0 150
      pkg/plugins/datasource/tsdb/datasource_plugin_wrapper.go
  27. 0 108
      pkg/plugins/datasource/tsdb/datasource_plugin_wrapper_test.go
  28. 0 22
      pkg/plugins/datasource/tsdb/grpc.go
  29. 0 27
      pkg/plugins/datasource/tsdb/interface.go
  30. 156 0
      pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go
  31. 109 0
      pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go
  32. 5 4
      pkg/plugins/datasource_plugin.go
  33. 1 1
      pkg/services/alerting/notifiers/email.go
  34. 12 2
      pkg/services/alerting/notifiers/opsgenie.go
  35. 10 5
      pkg/services/provisioning/dashboards/config_reader.go
  36. 18 7
      pkg/services/provisioning/dashboards/config_reader_test.go
  37. 3 2
      pkg/services/provisioning/dashboards/dashboard.go
  38. 19 2
      pkg/services/provisioning/dashboards/file_reader.go
  39. 29 5
      pkg/services/provisioning/dashboards/file_reader_test.go
  40. 2 2
      pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml
  41. 10 6
      pkg/services/provisioning/datasources/datasources.go
  42. 14 3
      pkg/services/provisioning/datasources/datasources_test.go
  43. 4 4
      pkg/services/sqlstore/alert.go
  44. 3 3
      pkg/services/sqlstore/alert_notification.go
  45. 1 0
      pkg/services/sqlstore/dashboard_acl.go
  46. 3 6
      pkg/services/sqlstore/playlist.go
  47. 64 61
      pkg/services/sqlstore/stats.go
  48. 116 43
      pkg/social/generic_oauth.go
  49. 2 2
      pkg/social/github_oauth.go
  50. 2 2
      pkg/social/google_oauth.go
  51. 2 2
      pkg/social/grafana_com_oauth.go
  52. 28 7
      pkg/social/social.go
  53. 50 16
      pkg/tsdb/cloudwatch/metric_find_query.go
  54. 82 0
      pkg/tsdb/cloudwatch/metric_find_query_test.go
  55. 0 636
      pkg/tsdb/models/tsdb_plugin.pb.go
  56. 0 98
      pkg/tsdb/models/tsdb_plugin.proto
  57. 71 59
      pkg/tsdb/mysql/mysql.go
  58. 43 32
      pkg/tsdb/mysql/mysql_test.go
  59. 6 0
      public/app/core/angular_wrappers.ts
  60. 37 0
      public/app/core/components/TagFilter/TagBadge.tsx
  61. 69 0
      public/app/core/components/TagFilter/TagFilter.tsx
  62. 52 0
      public/app/core/components/TagFilter/TagOption.tsx
  63. 26 0
      public/app/core/components/TagFilter/TagValue.tsx
  64. 22 9
      public/app/core/components/form_dropdown/form_dropdown.ts
  65. 18 28
      public/app/core/components/search/search.html
  66. 26 5
      public/app/core/components/search/search.ts
  67. 1 1
      public/app/core/directives/dropdown_typeahead.js
  68. 2 73
      public/app/core/directives/tags.ts
  69. 0 4
      public/app/core/services/segment_srv.js
  70. 2 0
      public/app/core/utils/kbn.ts
  71. 86 0
      public/app/core/utils/tags.ts
  72. 52 0
      public/app/core/utils/url.ts
  73. 1 1
      public/app/features/dashboard/dashboard_srv.ts
  74. 1 2
      public/app/features/dashboard/dashnav/dashnav.ts
  75. 5 13
      public/app/features/dashboard/folder_picker/folder_picker.html
  76. 5 16
      public/app/features/dashboard/save_as_modal.ts
  77. 3 1
      public/app/features/dashboard/settings/settings.html
  78. 6 0
      public/app/features/dashboard/settings/settings.ts
  79. 6 8
      public/app/features/templating/editor_ctrl.ts
  80. 4 4
      public/app/features/templating/partials/editor.html
  81. 40 0
      public/app/features/templating/specs/editor_ctrl.jest.ts
  82. 96 54
      public/app/plugins/datasource/graphite/add_graphite_func.js
  83. 0 1
      public/app/plugins/datasource/graphite/config_ctrl.ts
  84. 118 11
      public/app/plugins/datasource/graphite/datasource.ts
  85. 95 33
      public/app/plugins/datasource/graphite/func_editor.js
  86. 121 217
      public/app/plugins/datasource/graphite/gfunc.ts
  87. 7 6
      public/app/plugins/datasource/graphite/graphite_query.ts
  88. 35 15
      public/app/plugins/datasource/graphite/partials/query.editor.html
  89. 33 20
      public/app/plugins/datasource/graphite/query_ctrl.ts
  90. 6 5
      public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
  91. 20 6
      public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts
  92. 1 1
      public/app/plugins/datasource/influxdb/partials/annotations.editor.html
  93. 1 1
      public/app/plugins/datasource/mysql/response_parser.ts
  94. 250 74
      public/app/plugins/datasource/prometheus/completer.ts
  95. 34 12
      public/app/plugins/datasource/prometheus/mode-prometheus.js
  96. 69 18
      public/app/plugins/datasource/prometheus/specs/completer_specs.ts
  97. 7 7
      public/app/plugins/panel/graph/tab_display.html
  98. 2 2
      public/app/plugins/panel/singlestat/editor.html
  99. 4 0
      public/app/plugins/panel/singlestat/module.ts
  100. 26 0
      public/app/stores/ViewStore/ViewStore.jest.ts

+ 0 - 1
.editorconfig

@@ -8,7 +8,6 @@ charset = utf-8
 trim_trailing_whitespace = true
 trim_trailing_whitespace = true
 insert_final_newline = true
 insert_final_newline = true
 max_line_length = 120
 max_line_length = 120
-insert_final_newline = true
 
 
 [*.go]
 [*.go]
 indent_style = tab
 indent_style = tab

+ 7 - 0
CHANGELOG.md

@@ -16,6 +16,13 @@ The new grid engine is major upgrade for how you can position and move panels. I
 
 
 Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: 24, h: 5}`. Units are in grid dimensions (24 columns, 1 height unit 30px). Rows and Panels objects exist (together) in a flat array directly on the dashboard root object. Rows are not needed for layouts anymore and are mainly there for backward compatibility. Some panel plugins that do not respect their panel height might require an update.
 Dashboard panels and rows are positioned using a gridPos object `{x: 0, y: 0, w: 24, h: 5}`. Units are in grid dimensions (24 columns, 1 height unit 30px). Rows and Panels objects exist (together) in a flat array directly on the dashboard root object. Rows are not needed for layouts anymore and are mainly there for backward compatibility. Some panel plugins that do not respect their panel height might require an update.
 
 
+## New Features
+* **Alerting**: Add support for internal image store [#6922](https://github.com/grafana/grafana/issues/6922), thx [@FunkyM](https://github.com/FunkyM)
+
+## Minor
+* **Graph**: Don't hide graph display options (Lines/Points) when draw mode is unchecked [#9770](https://github.com/grafana/grafana/issues/9770), thx [@Jonnymcc](https://github.com/Jonnymcc)
+* **Prometheus**: Show label name in paren after by/without/on/ignoring/group_left/group_right [#9664](https://github.com/grafana/grafana/pull/9664), thx [@mtanda](https://github.com/mtanda)
+
 # 4.7.0 (unreleased / v4.7.x branch)
 # 4.7.0 (unreleased / v4.7.x branch)
 
 
 ## Breaking changes
 ## Breaking changes

+ 630 - 0
Gopkg.lock

@@ -0,0 +1,630 @@
+# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
+
+
+[[projects]]
+  name = "cloud.google.com/go"
+  packages = ["compute/metadata"]
+  revision = "767c40d6a2e058483c25fa193e963a22da17236d"
+  version = "v0.18.0"
+
+[[projects]]
+  name = "github.com/BurntSushi/toml"
+  packages = ["."]
+  revision = "b26d9c308763d68093482582cea63d69be07a0f0"
+  version = "v0.3.0"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/Unknwon/com"
+  packages = ["."]
+  revision = "7677a1d7c1137cd3dd5ba7a076d0c898a1ef4520"
+
+[[projects]]
+  name = "github.com/apache/thrift"
+  packages = ["lib/go/thrift"]
+  revision = "b2a4d4ae21c789b689dd162deb819665567f481c"
+  version = "0.10.0"
+
+[[projects]]
+  name = "github.com/aws/aws-sdk-go"
+  packages = [
+    "aws",
+    "aws/awserr",
+    "aws/awsutil",
+    "aws/client",
+    "aws/client/metadata",
+    "aws/corehandlers",
+    "aws/credentials",
+    "aws/credentials/ec2rolecreds",
+    "aws/credentials/endpointcreds",
+    "aws/credentials/stscreds",
+    "aws/defaults",
+    "aws/ec2metadata",
+    "aws/endpoints",
+    "aws/request",
+    "aws/session",
+    "aws/signer/v4",
+    "internal/shareddefaults",
+    "private/protocol",
+    "private/protocol/ec2query",
+    "private/protocol/query",
+    "private/protocol/query/queryutil",
+    "private/protocol/rest",
+    "private/protocol/restxml",
+    "private/protocol/xml/xmlutil",
+    "service/cloudwatch",
+    "service/ec2",
+    "service/ec2/ec2iface",
+    "service/s3",
+    "service/sts"
+  ]
+  revision = "decd990ddc5dcdf2f73309cbcab90d06b996ca28"
+  version = "v1.12.67"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/benbjohnson/clock"
+  packages = ["."]
+  revision = "7dc76406b6d3c05b5f71a86293cbcf3c4ea03b19"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/beorn7/perks"
+  packages = ["quantile"]
+  revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/bmizerany/assert"
+  packages = ["."]
+  revision = "b7ed37b82869576c289d7d97fb2bbd8b64a0cb28"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/bradfitz/gomemcache"
+  packages = ["memcache"]
+  revision = "1952afaa557dc08e8e0d89eafab110fb501c1a2b"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/codahale/hdrhistogram"
+  packages = ["."]
+  revision = "3a0bb77429bd3a61596f5e8a3172445844342120"
+
+[[projects]]
+  name = "github.com/codegangsta/cli"
+  packages = ["."]
+  revision = "cfb38830724cc34fedffe9a2a29fb54fa9169cd1"
+  version = "v1.20.0"
+
+[[projects]]
+  name = "github.com/davecgh/go-spew"
+  packages = ["spew"]
+  revision = "346938d642f2ec3594ed81d874461961cd0faa76"
+  version = "v1.1.0"
+
+[[projects]]
+  name = "github.com/fatih/color"
+  packages = ["."]
+  revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260"
+  version = "v1.5.0"
+
+[[projects]]
+  name = "github.com/go-ini/ini"
+  packages = ["."]
+  revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a"
+  version = "v1.32.0"
+
+[[projects]]
+  name = "github.com/go-ldap/ldap"
+  packages = ["."]
+  revision = "bb7a9ca6e4fbc2129e3db588a34bc970ffe811a9"
+  version = "v2.5.1"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/go-macaron/binding"
+  packages = ["."]
+  revision = "ac54ee249c27dca7e76fad851a4a04b73bd1b183"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/go-macaron/gzip"
+  packages = ["."]
+  revision = "cad1c6580a07c56f5f6bc52d66002a05985c5854"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/go-macaron/inject"
+  packages = ["."]
+  revision = "d8a0b8677191f4380287cfebd08e462217bac7ad"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/go-macaron/session"
+  packages = [
+    ".",
+    "memcache",
+    "mysql",
+    "postgres",
+    "redis"
+  ]
+  revision = "b8e286a0dba8f4999042d6b258daf51b31d08938"
+
+[[projects]]
+  name = "github.com/go-sql-driver/mysql"
+  packages = ["."]
+  revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
+
+[[projects]]
+  name = "github.com/go-stack/stack"
+  packages = ["."]
+  revision = "259ab82a6cad3992b4e21ff5cac294ccb06474bc"
+  version = "v1.7.0"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/go-xorm/builder"
+  packages = ["."]
+  revision = "488224409dd8aa2ce7a5baf8d10d55764a913738"
+
+[[projects]]
+  name = "github.com/go-xorm/core"
+  packages = ["."]
+  revision = "e8409d73255791843585964791443dbad877058c"
+
+[[projects]]
+  name = "github.com/go-xorm/xorm"
+  packages = ["."]
+  revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/golang/protobuf"
+  packages = [
+    "proto",
+    "ptypes",
+    "ptypes/any",
+    "ptypes/duration",
+    "ptypes/timestamp"
+  ]
+  revision = "c65a0412e71e8b9b3bfd22925720d23c0f054237"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/gopherjs/gopherjs"
+  packages = ["js"]
+  revision = "178c176a91fe05e3e6c58fa5c989bad19e6cdcb3"
+
+[[projects]]
+  name = "github.com/gorilla/websocket"
+  packages = ["."]
+  revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b"
+  version = "v1.2.0"
+
+[[projects]]
+  name = "github.com/gosimple/slug"
+  packages = ["."]
+  revision = "e9f42fa127660e552d0ad2b589868d403a9be7c6"
+  version = "v1.1.1"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/grafana/grafana_plugin_model"
+  packages = ["go/datasource"]
+  revision = "dfe5dc0a6ce05825ba7fe2d0323d92e631bffa89"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/hashicorp/go-hclog"
+  packages = ["."]
+  revision = "5bcb0f17e36442247290887cc914a6e507afa5c4"
+
+[[projects]]
+  name = "github.com/hashicorp/go-plugin"
+  packages = ["."]
+  revision = "3e6d191694b5a3a2b99755f31b47fa209e4bcd09"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/hashicorp/go-version"
+  packages = ["."]
+  revision = "4fe82ae3040f80a03d04d2cccb5606a626b8e1ee"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/hashicorp/yamux"
+  packages = ["."]
+  revision = "683f49123a33db61abfb241b7ac5e4af4dc54d55"
+
+[[projects]]
+  name = "github.com/inconshreveable/log15"
+  packages = ["."]
+  revision = "0decfc6c20d9ca0ad143b0e89dcaa20f810b4fb3"
+  version = "v2.13"
+
+[[projects]]
+  name = "github.com/jmespath/go-jmespath"
+  packages = ["."]
+  revision = "0b12d6b5"
+
+[[projects]]
+  name = "github.com/jtolds/gls"
+  packages = ["."]
+  revision = "77f18212c9c7edc9bd6a33d383a7b545ce62f064"
+  version = "v4.2.1"
+
+[[projects]]
+  name = "github.com/klauspost/compress"
+  packages = [
+    "flate",
+    "gzip"
+  ]
+  revision = "6c8db69c4b49dd4df1fff66996cf556176d0b9bf"
+  version = "v1.2.1"
+
+[[projects]]
+  name = "github.com/klauspost/cpuid"
+  packages = ["."]
+  revision = "ae7887de9fa5d2db4eaa8174a7eff2c1ac00f2da"
+  version = "v1.1"
+
+[[projects]]
+  name = "github.com/klauspost/crc32"
+  packages = ["."]
+  revision = "cb6bfca970f6908083f26f39a79009d608efd5cd"
+  version = "v1.1"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/kr/pretty"
+  packages = ["."]
+  revision = "cfb55aafdaf3ec08f0db22699ab822c50091b1c4"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/kr/text"
+  packages = ["."]
+  revision = "7cafcd837844e784b526369c9bce262804aebc60"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/lib/pq"
+  packages = [
+    ".",
+    "oid"
+  ]
+  revision = "61fe37aa2ee24fabcdbe5c4ac1d4ac566f88f345"
+
+[[projects]]
+  name = "github.com/mattn/go-colorable"
+  packages = ["."]
+  revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
+  version = "v0.0.9"
+
+[[projects]]
+  name = "github.com/mattn/go-isatty"
+  packages = ["."]
+  revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
+  version = "v0.0.3"
+
+[[projects]]
+  name = "github.com/mattn/go-sqlite3"
+  packages = ["."]
+  revision = "6c771bb9887719704b210e87e934f08be014bdb1"
+  version = "v1.6.0"
+
+[[projects]]
+  name = "github.com/matttproud/golang_protobuf_extensions"
+  packages = ["pbutil"]
+  revision = "3247c84500bff8d9fb6d579d800f20b3e091582c"
+  version = "v1.0.0"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/mitchellh/go-testing-interface"
+  packages = ["."]
+  revision = "a61a99592b77c9ba629d254a693acffaeb4b7e28"
+
+[[projects]]
+  name = "github.com/opentracing/opentracing-go"
+  packages = [
+    ".",
+    "ext",
+    "log"
+  ]
+  revision = "1949ddbfd147afd4d964a9f00b24eb291e0e7c38"
+  version = "v1.0.2"
+
+[[projects]]
+  name = "github.com/patrickmn/go-cache"
+  packages = ["."]
+  revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0"
+  version = "v2.1.0"
+
+[[projects]]
+  name = "github.com/prometheus/client_golang"
+  packages = [
+    "api",
+    "api/prometheus/v1",
+    "prometheus",
+    "prometheus/promhttp"
+  ]
+  revision = "967789050ba94deca04a5e84cce8ad472ce313c1"
+  version = "v0.9.0-pre1"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/prometheus/client_model"
+  packages = ["go"]
+  revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/prometheus/common"
+  packages = [
+    "expfmt",
+    "internal/bitbucket.org/ww/goautoneg",
+    "model"
+  ]
+  revision = "89604d197083d4781071d3c65855d24ecfb0a563"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/prometheus/procfs"
+  packages = [
+    ".",
+    "internal/util",
+    "nfsd",
+    "xfs"
+  ]
+  revision = "85fadb6e89903ef7cca6f6a804474cd5ea85b6e1"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/rainycape/unidecode"
+  packages = ["."]
+  revision = "cb7f23ec59bec0d61b19c56cd88cee3d0cc1870c"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/sergi/go-diff"
+  packages = ["diffmatchpatch"]
+  revision = "1744e2970ca51c86172c8190fadad617561ed6e7"
+
+[[projects]]
+  name = "github.com/smartystreets/assertions"
+  packages = [
+    ".",
+    "internal/go-render/render",
+    "internal/oglematchers"
+  ]
+  revision = "0b37b35ec7434b77e77a4bb29b79677cced992ea"
+  version = "1.8.1"
+
+[[projects]]
+  name = "github.com/smartystreets/goconvey"
+  packages = [
+    "convey",
+    "convey/gotest",
+    "convey/reporting"
+  ]
+  revision = "9e8dc3f972df6c8fcc0375ef492c24d0bb204857"
+  version = "1.6.3"
+
+[[projects]]
+  name = "github.com/uber/jaeger-client-go"
+  packages = [
+    ".",
+    "config",
+    "internal/baggage",
+    "internal/baggage/remote",
+    "internal/spanlog",
+    "log",
+    "rpcmetrics",
+    "thrift-gen/agent",
+    "thrift-gen/baggage",
+    "thrift-gen/jaeger",
+    "thrift-gen/sampling",
+    "thrift-gen/zipkincore",
+    "utils"
+  ]
+  revision = "3ac96c6e679cb60a74589b0d0aa7c70a906183f7"
+  version = "v2.11.2"
+
+[[projects]]
+  name = "github.com/uber/jaeger-lib"
+  packages = ["metrics"]
+  revision = "7f95f4f7e80028096410abddaae2556e4c61b59f"
+  version = "v1.3.1"
+
+[[projects]]
+  name = "github.com/yudai/gojsondiff"
+  packages = [
+    ".",
+    "formatter"
+  ]
+  revision = "7b1b7adf999dab73a6eb02669c3d82dbb27a3dd6"
+  version = "1.0.0"
+
+[[projects]]
+  branch = "master"
+  name = "github.com/yudai/golcs"
+  packages = ["."]
+  revision = "ecda9a501e8220fae3b4b600c3db4b0ba22cfc68"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/crypto"
+  packages = ["pbkdf2"]
+  revision = "3d37316aaa6bd9929127ac9a527abf408178ea7b"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/net"
+  packages = [
+    "context",
+    "context/ctxhttp",
+    "http2",
+    "http2/hpack",
+    "idna",
+    "internal/timeseries",
+    "lex/httplex",
+    "trace"
+  ]
+  revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/oauth2"
+  packages = [
+    ".",
+    "google",
+    "internal",
+    "jws",
+    "jwt"
+  ]
+  revision = "b28fcf2b08a19742b43084fb40ab78ac6c3d8067"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/sync"
+  packages = ["errgroup"]
+  revision = "fd80eb99c8f653c847d294a001bdf2a3a6f768f5"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/sys"
+  packages = ["unix"]
+  revision = "af50095a40f9041b3b38960738837185c26e9419"
+
+[[projects]]
+  branch = "master"
+  name = "golang.org/x/text"
+  packages = [
+    "collate",
+    "collate/build",
+    "internal/colltab",
+    "internal/gen",
+    "internal/tag",
+    "internal/triegen",
+    "internal/ucd",
+    "language",
+    "secure/bidirule",
+    "transform",
+    "unicode/bidi",
+    "unicode/cldr",
+    "unicode/norm",
+    "unicode/rangetable"
+  ]
+  revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3"
+
+[[projects]]
+  name = "google.golang.org/appengine"
+  packages = [
+    ".",
+    "cloudsql",
+    "internal",
+    "internal/app_identity",
+    "internal/base",
+    "internal/datastore",
+    "internal/log",
+    "internal/modules",
+    "internal/remote_api",
+    "internal/urlfetch",
+    "urlfetch"
+  ]
+  revision = "150dc57a1b433e64154302bdc40b6bb8aefa313a"
+  version = "v1.0.0"
+
+[[projects]]
+  branch = "master"
+  name = "google.golang.org/genproto"
+  packages = ["googleapis/rpc/status"]
+  revision = "a8101f21cf983e773d0c1133ebc5424792003214"
+
+[[projects]]
+  name = "google.golang.org/grpc"
+  packages = [
+    ".",
+    "balancer",
+    "balancer/base",
+    "balancer/roundrobin",
+    "codes",
+    "connectivity",
+    "credentials",
+    "encoding",
+    "grpclb/grpc_lb_v1/messages",
+    "grpclog",
+    "health",
+    "health/grpc_health_v1",
+    "internal",
+    "keepalive",
+    "metadata",
+    "naming",
+    "peer",
+    "resolver",
+    "resolver/dns",
+    "resolver/passthrough",
+    "stats",
+    "status",
+    "tap",
+    "transport"
+  ]
+  revision = "6b51017f791ae1cfbec89c52efdf444b13b550ef"
+  version = "v1.9.2"
+
+[[projects]]
+  branch = "v3"
+  name = "gopkg.in/alexcesaro/quotedprintable.v3"
+  packages = ["."]
+  revision = "2caba252f4dc53eaf6b553000885530023f54623"
+
+[[projects]]
+  name = "gopkg.in/asn1-ber.v1"
+  packages = ["."]
+  revision = "379148ca0225df7a432012b8df0355c2a2063ac0"
+  version = "v1.2"
+
+[[projects]]
+  name = "gopkg.in/bufio.v1"
+  packages = ["."]
+  revision = "567b2bfa514e796916c4747494d6ff5132a1dfce"
+  version = "v1"
+
+[[projects]]
+  branch = "v2"
+  name = "gopkg.in/gomail.v2"
+  packages = ["."]
+  revision = "81ebce5c23dfd25c6c67194b37d3dd3f338c98b1"
+
+[[projects]]
+  name = "gopkg.in/ini.v1"
+  packages = ["."]
+  revision = "32e4c1e6bc4e7d0d8451aa6b75200d19e37a536a"
+  version = "v1.32.0"
+
+[[projects]]
+  name = "gopkg.in/macaron.v1"
+  packages = ["."]
+  revision = "75f2e9b42e99652f0d82b28ccb73648f44615faa"
+  version = "v1.2.4"
+
+[[projects]]
+  name = "gopkg.in/redis.v2"
+  packages = ["."]
+  revision = "e6179049628164864e6e84e973cfb56335748dea"
+  version = "v2.3.2"
+
+[[projects]]
+  branch = "v2"
+  name = "gopkg.in/yaml.v2"
+  packages = ["."]
+  revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4"
+
+[solve-meta]
+  analyzer-name = "dep"
+  analyzer-version = 1
+  inputs-digest = "98e8d8f5fb21fe448aeb3db41c9fed85fe3bf80400e553211cf39a9c05720e01"
+  solver-name = "gps-cdcl"
+  solver-version = 1

+ 195 - 0
Gopkg.toml

@@ -0,0 +1,195 @@
+# Gopkg.toml example
+#
+# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
+# for detailed Gopkg.toml documentation.
+#
+# required = ["github.com/user/thing/cmd/thing"]
+# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
+#
+# [[constraint]]
+#   name = "github.com/user/project"
+#   version = "1.0.0"
+#
+# [[constraint]]
+#   name = "github.com/user/project2"
+#   branch = "dev"
+#   source = "github.com/myfork/project2"
+#
+# [[override]]
+#  name = "github.com/x/y"
+#  version = "2.4.0"
+
+ignored = [
+  "github.com/grafana/grafana/data/*",
+  "github.com/grafana/grafana/public/*",
+  "github.com/grafana/grafana/node_modules/*"
+ ]
+
+[[constraint]]
+  name = "github.com/BurntSushi/toml"
+  version = "0.3.0"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/Unknwon/com"
+  #version = "1.0.0"
+
+[[constraint]]
+  name = "github.com/aws/aws-sdk-go"
+  version = "1.12.65"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/benbjohnson/clock"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/bmizerany/assert"
+
+[[constraint]]
+  name = "github.com/codegangsta/cli"
+  version = "1.20.0"
+
+[[constraint]]
+  name = "github.com/davecgh/go-spew"
+  version = "1.1.0"
+
+[[constraint]]
+  name = "github.com/fatih/color"
+  version = "1.5.0"
+
+[[constraint]]
+  name = "github.com/go-ldap/ldap"
+  version = "2.5.1"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/go-macaron/binding"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/go-macaron/gzip"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/go-macaron/session"
+
+[[constraint]]
+  name = "github.com/go-sql-driver/mysql"
+  revision = "2cc627ac8defc45d65066ae98f898166f580f9a4"
+  #version = "1.3.0" //keeping this since we would rather depend on version then commit
+
+[[constraint]]
+  name = "github.com/go-stack/stack"
+  version = "1.7.0"
+
+[[constraint]]
+  name = "github.com/go-xorm/core"
+  revision = "e8409d73255791843585964791443dbad877058c"
+  #version = "0.5.7" //keeping this since we would rather depend on version then commit
+
+[[constraint]]
+  name = "github.com/go-xorm/xorm"
+  revision = "6687a2b4e824f4d87f2d65060ec5cb0d896dff1e"
+  #version = "0.6.4" //keeping this since we would rather depend on version then commit
+
+[[constraint]]
+  name = "github.com/gorilla/websocket"
+  version = "1.2.0"
+
+[[constraint]]
+  name = "github.com/gosimple/slug"
+  version = "1.1.1"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/grafana/grafana_plugin_model"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/hashicorp/go-hclog"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/hashicorp/go-version"
+
+[[constraint]]
+  name = "github.com/inconshreveable/log15"
+  version = "2.13.0"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/lib/pq"
+
+[[constraint]]
+  name = "github.com/mattn/go-isatty"
+  version = "0.0.3"
+
+[[constraint]]
+  name = "github.com/mattn/go-sqlite3"
+  version = "1.6.0"
+
+[[constraint]]
+  name = "github.com/opentracing/opentracing-go"
+  version = "1.0.2"
+
+[[constraint]]
+  name = "github.com/patrickmn/go-cache"
+  version = "2.1.0"
+
+[[constraint]]
+  name = "github.com/prometheus/client_golang"
+  version = "0.9.0-pre1"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/prometheus/client_model"
+
+[[constraint]]
+  branch = "master"
+  name = "github.com/prometheus/common"
+
+[[constraint]]
+  name = "github.com/smartystreets/goconvey"
+  version = "1.6.3"
+
+[[constraint]]
+  name = "github.com/uber/jaeger-client-go"
+  version = "2.11.2"
+
+[[constraint]]
+  name = "github.com/yudai/gojsondiff"
+  version = "1.0.0"
+
+[[constraint]]
+  branch = "master"
+  name = "golang.org/x/net"
+
+[[constraint]]
+  branch = "master"
+  name = "golang.org/x/oauth2"
+
+[[constraint]]
+  branch = "master"
+  name = "golang.org/x/sync"
+
+[[constraint]]
+  name = "gopkg.in/gomail.v2"
+  branch = "v2"
+
+[[constraint]]
+  name = "gopkg.in/ini.v1"
+  version = "1.32.0"
+
+[[constraint]]
+  name = "gopkg.in/macaron.v1"
+  version = "1.2.4"
+
+[[constraint]]
+  branch = "v2"
+  name = "gopkg.in/yaml.v2"
+
+[prune]
+  non-go = true
+  go-tests = true
+  unused-packages = true

+ 4 - 10
README.md

@@ -45,23 +45,17 @@ For this you need nodejs (v.6+).
 ```bash
 ```bash
 npm install -g yarn
 npm install -g yarn
 yarn install --pure-lockfile
 yarn install --pure-lockfile
-npm run build
-```
-
-To rebuild frontend assets (typescript, sass etc) as you change them start the watcher via.
-
-```bash
 npm run watch
 npm run watch
 ```
 ```
 
 
-Run tests
+Run tests 
 ```bash
 ```bash
-npm run test
+npm run jest
 ```
 ```
 
 
-Run tests in watch mode
+Run karma tests
 ```bash
 ```bash
-npm run watch-test
+npm run karma
 ```
 ```
 
 
 ### Recompile backend on source change
 ### Recompile backend on source change

+ 6 - 6
build.go

@@ -347,11 +347,11 @@ func ChangeWorkingDir(dir string) {
 }
 }
 
 
 func grunt(params ...string) {
 func grunt(params ...string) {
-  if runtime.GOOS == "windows" {
-    runPrint(`.\node_modules\.bin\grunt`, params...)
-  } else {
-    runPrint("./node_modules/.bin/grunt", params...)
-  }
+	if runtime.GOOS == "windows" {
+		runPrint(`.\node_modules\.bin\grunt`, params...)
+	} else {
+		runPrint("./node_modules/.bin/grunt", params...)
+	}
 }
 }
 
 
 func gruntBuildArg(task string) []string {
 func gruntBuildArg(task string) []string {
@@ -371,7 +371,7 @@ func gruntBuildArg(task string) []string {
 }
 }
 
 
 func setup() {
 func setup() {
-	runPrint("go", "get", "-v", "github.com/kardianos/govendor")
+	runPrint("go", "get", "-v", "github.com/golang/dep")
 	runPrint("go", "install", "-v", "./pkg/cmd/grafana-server")
 	runPrint("go", "install", "-v", "./pkg/cmd/grafana-server")
 }
 }
 
 

+ 4 - 1
conf/defaults.ini

@@ -473,7 +473,7 @@ sampler_param = 1
 
 
 #################################### External Image Storage ##############
 #################################### External Image Storage ##############
 [external_image_storage]
 [external_image_storage]
-# You can choose between (s3, webdav, gcs, azure_blob)
+# You can choose between (s3, webdav, gcs, azure_blob, local)
 provider =
 provider =
 
 
 [external_image_storage.s3]
 [external_image_storage.s3]
@@ -499,3 +499,6 @@ path =
 account_name =
 account_name =
 account_key =
 account_key =
 container_name =
 container_name =
+
+[external_image_storage.local]
+# does not require any configuration

+ 4 - 1
conf/sample.ini

@@ -417,7 +417,7 @@ log_queries =
 #################################### External image storage ##########################
 #################################### External image storage ##########################
 [external_image_storage]
 [external_image_storage]
 # Used for uploading images to public servers so they can be included in slack/email messages.
 # Used for uploading images to public servers so they can be included in slack/email messages.
-# you can choose between (s3, webdav, gcs, azure_blob)
+# you can choose between (s3, webdav, gcs, azure_blob, local)
 ;provider =
 ;provider =
 
 
 [external_image_storage.s3]
 [external_image_storage.s3]
@@ -442,3 +442,6 @@ log_queries =
 ;account_name =
 ;account_name =
 ;account_key =
 ;account_key =
 ;container_name =
 ;container_name =
+
+[external_image_storage.local]
+# does not require any configuration

+ 7 - 0
docker/blocks/mysql/docker-compose.yaml

@@ -12,3 +12,10 @@
       - /etc/timezone:/etc/timezone:ro
       - /etc/timezone:/etc/timezone:ro
     command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all]
     command: [mysqld, --character-set-server=utf8mb4, --collation-server=utf8mb4_unicode_ci, --innodb_monitor_enable=all]
 
 
+  fake-mysql-data:
+    image: grafana/fake-data-gen
+    network_mode: bridge
+    environment:
+      FD_DATASOURCE: mysql
+      FD_PORT: 3306
+

+ 3 - 0
docker/blocks/mysql_tests/docker-compose.yaml

@@ -7,4 +7,7 @@
       MYSQL_PASSWORD: password
       MYSQL_PASSWORD: password
     ports:
     ports:
       - "3306:3306"
       - "3306:3306"
+    volumes:
+      - /etc/localtime:/etc/localtime:ro
+      - /etc/timezone:/etc/timezone:ro
     tmpfs: /var/lib/mysql:rw
     tmpfs: /var/lib/mysql:rw

+ 7 - 0
docker/blocks/postgres/docker-compose.yaml

@@ -7,3 +7,10 @@
     ports:
     ports:
       - "5432:5432"
       - "5432:5432"
     command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql
     command: postgres -c log_connections=on -c logging_collector=on -c log_destination=stderr -c log_directory=/var/log/postgresql
+
+  fake-postgres-data:
+    image: grafana/fake-data-gen
+    network_mode: bridge
+    environment:
+      FD_DATASOURCE: postgres
+      FD_PORT: 5432

+ 4 - 2
docs/sources/alerting/notifications.md

@@ -149,8 +149,10 @@ Prometheus Alertmanager | `prometheus-alertmanager` | no
 
 
 # Enable images in notifications {#external-image-store}
 # Enable images in notifications {#external-image-store}
 
 
-Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessible (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
-Amazon S3, Webdav, and Azure Blob Storage for this. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file.
+Grafana can render the panel associated with the alert rule and include that in the notification. Most Notification Channels require that this image be publicly accessable (Slack and PagerDuty for example). In order to include images in alert notifications, Grafana can upload the image to an image store. It currently supports
+Amazon S3, Webdav, Google Cloud Storage and Azure Blob Storage. So to set that up you need to configure the [external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini config file.
+
+Be aware that some notifiers requires public access to the image to be able to include it in the notification. So make sure to enable public access to the images. If your using local image uploader, your Grafana instance need to be accessible by the internet.
 
 
 Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store.
 Currently only the Email Channels attaches images if no external image store is specified. To include images in alert notifications for other channels then you need to set up an external image store.
 
 

+ 17 - 0
docs/sources/installation/behind_proxy.md

@@ -68,6 +68,23 @@ server {
   }
   }
 }
 }
 ```
 ```
+
+#### HAProxy configuration with sub path
+```bash
+frontend http-in
+  bind *:80
+  use_backend grafana_backend if { path /grafana } or { path_beg /grafana/ }
+
+backend grafana_backend
+  # Requires haproxy >= 1.6
+  http-request set-path %[path,regsub(^/grafana/?,/)]
+
+  # Works for haproxy < 1.6
+  # reqrep ^([^\ ]*\ /)grafana[/]?(.*) \1\2
+
+  server grafana localhost:3000
+```
+
 ### IIS URL Rewrite Rule (Windows) with Subpath
 ### IIS URL Rewrite Rule (Windows) with Subpath
 
 
 IIS requires that the URL Rewrite module is installed.
 IIS requires that the URL Rewrite module is installed.

+ 65 - 1
docs/sources/installation/configuration.md

@@ -540,6 +540,70 @@ allowed_organizations =
     allowed_organizations =
     allowed_organizations =
     ```
     ```
 
 
+### Set up oauth2 with Auth0
+
+1.  Create a new Client in Auth0
+    - Name: Grafana
+    - Type: Regular Web Application
+
+2.  Go to the Settings tab and set:
+    - Allowed Callback URLs: `https://<grafana domain>/login/generic_oauth`
+
+3. Click Save Changes, then use the values at the top of the page to configure Grafana:
+
+    ```bash
+    [auth.generic_oauth]
+    enabled = true
+    allow_sign_up = true
+    team_ids =
+    allowed_organizations =
+    name = Auth0
+    client_id = <client id>
+    client_secret = <client secret>
+    scopes = openid profile email
+    auth_url = https://<domain>/authorize
+    token_url = https://<domain>/oauth/token
+    api_url = https://<domain>/userinfo
+    ```
+
+### Set up oauth2 with Azure Active Directory
+
+1.  Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item.
+
+2.  Copy the "Directory ID", this is needed for setting URLs later
+
+3.  Click "App Registrations" and add a new application registration:
+    - Name: Grafana
+    - Application type: Web app / API
+    - Sign-on URL: `https://<grafana domain>/login/generic_oauth`
+
+4.  Click the name of the new application to open the application details page.
+
+5.  Note down the "Application ID", this will be the OAuth client id.
+
+6.  Click "Settings", then click "Keys" and add a new entry under Passwords
+    - Key Description: Grafana OAuth
+    - Duration: Never Expires
+
+7.  Click Save then copy the key value, this will be the OAuth client secret.
+
+8.  Configure Grafana as follows:
+
+    ```bash
+    [auth.generic_oauth]
+    name = Azure AD
+    enabled = true
+    allow_sign_up = true
+    client_id = <application id>
+    client_secret = <key value>
+    scopes = openid email name
+    auth_url = https://login.microsoftonline.com/<directory id>/oauth2/authorize
+    token_url = https://login.microsoftonline.com/<directory id>/oauth2/token
+    api_url =
+    team_ids =
+    allowed_organizations =
+    ```
+
 <hr>
 <hr>
 
 
 ## [auth.basic]
 ## [auth.basic]
@@ -766,7 +830,7 @@ Time to live for snapshots.
 These options control how images should be made public so they can be shared on services like slack.
 These options control how images should be made public so they can be shared on services like slack.
 
 
 ### provider
 ### provider
-You can choose between (s3, webdav, gcs, azure_blob). If left empty Grafana will ignore the upload action.
+You can choose between (s3, webdav, gcs, azure_blob, local). If left empty Grafana will ignore the upload action.
 
 
 ## [external_image_storage.s3]
 ## [external_image_storage.s3]
 
 

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

@@ -57,7 +57,7 @@ For this you need nodejs (v.6+).
 ```bash
 ```bash
 npm install -g yarn
 npm install -g yarn
 yarn install --pure-lockfile
 yarn install --pure-lockfile
-npm run build
+npm run watch
 ```
 ```
 
 
 ## Running Grafana Locally
 ## Running Grafana Locally

+ 4 - 4
package.json

@@ -91,8 +91,7 @@
     "typescript": "^2.6.2",
     "typescript": "^2.6.2",
     "webpack": "^3.10.0",
     "webpack": "^3.10.0",
     "webpack-bundle-analyzer": "^2.9.0",
     "webpack-bundle-analyzer": "^2.9.0",
-    "webpack-cleanup-plugin": "^0.5.1",
-    "angular-mocks": "^1.6.6",
+    "webpack-cleanup-plugin": "^0.5.1",    
     "webpack-merge": "^4.1.0",
     "webpack-merge": "^4.1.0",
     "zone.js": "^0.7.2"
     "zone.js": "^0.7.2"
   },
   },
@@ -135,7 +134,7 @@
     "clipboard": "^1.7.1",
     "clipboard": "^1.7.1",
     "d3": "^4.11.0",
     "d3": "^4.11.0",
     "d3-scale-chromatic": "^1.1.1",
     "d3-scale-chromatic": "^1.1.1",
-    "eventemitter3": "^2.0.2",
+    "eventemitter3": "^2.0.3",
     "file-saver": "^1.3.3",
     "file-saver": "^1.3.3",
     "jquery": "^3.2.1",
     "jquery": "^3.2.1",
     "lodash": "^4.17.4",
     "lodash": "^4.17.4",
@@ -148,12 +147,13 @@
     "prop-types": "^15.6.0",
     "prop-types": "^15.6.0",
     "react": "^16.2.0",
     "react": "^16.2.0",
     "react-dom": "^16.2.0",
     "react-dom": "^16.2.0",
-    "react-grid-layout": "^0.16.1",
+    "react-grid-layout": "^0.16.2",
     "react-popper": "^0.7.5",
     "react-popper": "^0.7.5",
     "react-highlight-words": "^0.10.0",
     "react-highlight-words": "^0.10.0",
     "react-select": "^1.1.0",
     "react-select": "^1.1.0",
     "react-sizeme": "^2.3.6",
     "react-sizeme": "^2.3.6",
     "remarkable": "^1.7.1",
     "remarkable": "^1.7.1",
+    "rst2html": "github:thoward/rst2html#990cb89",
     "rxjs": "^5.4.3",
     "rxjs": "^5.4.3",
     "tether": "^1.4.0",
     "tether": "^1.4.0",
     "tether-drop": "https://github.com/torkelo/drop",
     "tether-drop": "https://github.com/torkelo/drop",

+ 4 - 0
pkg/api/http_server.go

@@ -162,6 +162,10 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron {
 	hs.mapStatic(m, setting.StaticRootPath, "", "public")
 	hs.mapStatic(m, setting.StaticRootPath, "", "public")
 	hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 	hs.mapStatic(m, setting.StaticRootPath, "robots.txt", "robots.txt")
 
 
+	if setting.ImageUploadProvider == "local" {
+		hs.mapStatic(m, setting.ImagesDir, "", "/public/img/attachments")
+	}
+
 	m.Use(macaron.Renderer(macaron.RenderOptions{
 	m.Use(macaron.Renderer(macaron.RenderOptions{
 		Directory:  path.Join(setting.StaticRootPath, "views"),
 		Directory:  path.Join(setting.StaticRootPath, "views"),
 		IndentJSON: macaron.Env != macaron.PROD,
 		IndentJSON: macaron.Env != macaron.PROD,

+ 12 - 9
pkg/api/login_oauth.go

@@ -1,6 +1,7 @@
 package api
 package api
 
 
 import (
 import (
+	"context"
 	"crypto/rand"
 	"crypto/rand"
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
@@ -11,7 +12,6 @@ import (
 	"net/http"
 	"net/http"
 	"net/url"
 	"net/url"
 
 
-	"golang.org/x/net/context"
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2"
 
 
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/bus"
@@ -29,7 +29,7 @@ var (
 	ErrSignUpNotAllowed      = errors.New("Signup is not allowed for this adapter")
 	ErrSignUpNotAllowed      = errors.New("Signup is not allowed for this adapter")
 	ErrUsersQuotaReached     = errors.New("Users quota reached")
 	ErrUsersQuotaReached     = errors.New("Users quota reached")
 	ErrNoEmail               = errors.New("Login provider didn't return an email address")
 	ErrNoEmail               = errors.New("Login provider didn't return an email address")
-	oauthLogger              = log.New("oauth.login")
+	oauthLogger              = log.New("oauth")
 )
 )
 
 
 func GenStateString() string {
 func GenStateString() string {
@@ -96,7 +96,9 @@ func OAuthLogin(ctx *middleware.Context) {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCert != "" || setting.OAuthService.OAuthInfos[name].TlsClientKey != "" {
 		cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
 		cert, err := tls.LoadX509KeyPair(setting.OAuthService.OAuthInfos[name].TlsClientCert, setting.OAuthService.OAuthInfos[name].TlsClientKey)
 		if err != nil {
 		if err != nil {
-			log.Fatal(1, "Failed to setup TlsClientCert", "oauth provider", name, "error", err)
+			ctx.Logger.Error("Failed to setup TlsClientCert", "oauth", name, "error", err)
+			ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCert)", nil)
+			return
 		}
 		}
 
 
 		tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
 		tr.TLSClientConfig.Certificates = append(tr.TLSClientConfig.Certificates, cert)
@@ -105,7 +107,9 @@ func OAuthLogin(ctx *middleware.Context) {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
 	if setting.OAuthService.OAuthInfos[name].TlsClientCa != "" {
 		caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
 		caCert, err := ioutil.ReadFile(setting.OAuthService.OAuthInfos[name].TlsClientCa)
 		if err != nil {
 		if err != nil {
-			log.Fatal(1, "Failed to setup TlsClientCa", "oauth provider", name, "error", err)
+			ctx.Logger.Error("Failed to setup TlsClientCa", "oauth", name, "error", err)
+			ctx.Handle(500, "login.OAuthLogin(Failed to setup TlsClientCa)", nil)
+			return
 		}
 		}
 		caCertPool := x509.NewCertPool()
 		caCertPool := x509.NewCertPool()
 		caCertPool.AppendCertsFromPEM(caCert)
 		caCertPool.AppendCertsFromPEM(caCert)
@@ -124,13 +128,13 @@ func OAuthLogin(ctx *middleware.Context) {
 	// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
 	// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
 	token.TokenType = "Bearer"
 	token.TokenType = "Bearer"
 
 
-	ctx.Logger.Debug("OAuthLogin Got token")
+	oauthLogger.Debug("OAuthLogin Got token", "token", token)
 
 
 	// set up oauth2 client
 	// set up oauth2 client
 	client := connect.Client(oauthCtx, token)
 	client := connect.Client(oauthCtx, token)
 
 
 	// get user info
 	// get user info
-	userInfo, err := connect.UserInfo(client)
+	userInfo, err := connect.UserInfo(client, token)
 	if err != nil {
 	if err != nil {
 		if sErr, ok := err.(*social.Error); ok {
 		if sErr, ok := err.(*social.Error); ok {
 			redirectWithError(ctx, sErr)
 			redirectWithError(ctx, sErr)
@@ -140,7 +144,7 @@ func OAuthLogin(ctx *middleware.Context) {
 		return
 		return
 	}
 	}
 
 
-	ctx.Logger.Debug("OAuthLogin got user info", "userInfo", userInfo)
+	oauthLogger.Debug("OAuthLogin got user info", "userInfo", userInfo)
 
 
 	// validate that we got at least an email address
 	// validate that we got at least an email address
 	if userInfo.Email == "" {
 	if userInfo.Email == "" {
@@ -205,8 +209,7 @@ func OAuthLogin(ctx *middleware.Context) {
 }
 }
 
 
 func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
 func redirectWithError(ctx *middleware.Context, err error, v ...interface{}) {
-	ctx.Logger.Info(err.Error(), v...)
-	// TODO: we can use the flash storage here once it's implemented
+	ctx.Logger.Error(err.Error(), v...)
 	ctx.Session.Set("loginError", err.Error())
 	ctx.Session.Set("loginError", err.Error())
 	ctx.Redirect(setting.AppSubUrl + "/login")
 	ctx.Redirect(setting.AppSubUrl + "/login")
 }
 }

+ 2 - 0
pkg/components/imguploader/imguploader.go

@@ -88,6 +88,8 @@ func NewImageUploader() (ImageUploader, error) {
 		container_name := azureBlobSec.Key("container_name").MustString("")
 		container_name := azureBlobSec.Key("container_name").MustString("")
 
 
 		return NewAzureBlobUploader(account_name, account_key, container_name), nil
 		return NewAzureBlobUploader(account_name, account_key, container_name), nil
+	case "local":
+		return NewLocalImageUploader()
 	}
 	}
 
 
 	if setting.ImageUploadProvider != "" {
 	if setting.ImageUploadProvider != "" {

+ 18 - 0
pkg/components/imguploader/imguploader_test.go

@@ -143,5 +143,23 @@ func TestImageUploaderFactory(t *testing.T) {
 				So(original.container_name, ShouldEqual, "container_name")
 				So(original.container_name, ShouldEqual, "container_name")
 			})
 			})
 		})
 		})
+
+		Convey("Local uploader", func() {
+			var err error
+
+			setting.NewConfigContext(&setting.CommandLineArgs{
+				HomePath: "../../../",
+			})
+
+			setting.ImageUploadProvider = "local"
+
+			uploader, err := NewImageUploader()
+
+			So(err, ShouldBeNil)
+			original, ok := uploader.(*LocalUploader)
+
+			So(ok, ShouldBeTrue)
+			So(original, ShouldNotBeNil)
+		})
 	})
 	})
 }
 }

+ 22 - 0
pkg/components/imguploader/localuploader.go

@@ -0,0 +1,22 @@
+package imguploader
+
+import (
+	"context"
+	"path"
+	"path/filepath"
+
+	"github.com/grafana/grafana/pkg/setting"
+)
+
+type LocalUploader struct {
+}
+
+func (u *LocalUploader) Upload(ctx context.Context, imageOnDiskPath string) (string, error) {
+	filename := filepath.Base(imageOnDiskPath)
+	image_url := setting.ToAbsUrl(path.Join("public/img/attachments", filename))
+	return image_url, nil
+}
+
+func NewLocalImageUploader() (*LocalUploader, error) {
+	return &LocalUploader{}, nil
+}

+ 18 - 0
pkg/components/imguploader/localuploader_test.go

@@ -0,0 +1,18 @@
+package imguploader
+
+import (
+	"context"
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestUploadToLocal(t *testing.T) {
+	Convey("[Integration test] for external_image_store.local", t, func() {
+		localUploader, _ := NewLocalImageUploader()
+		path, err := localUploader.Upload(context.Background(), "../../../public/img/logo_transparent_400x.png")
+
+		So(err, ShouldBeNil)
+		So(path, ShouldContainSubstring, "/public/img/attachments")
+	})
+}

+ 2 - 1
pkg/metrics/graphitebridge/graphite.go

@@ -26,9 +26,10 @@ import (
 	"strings"
 	"strings"
 	"time"
 	"time"
 
 
+	"context"
+
 	"github.com/prometheus/common/expfmt"
 	"github.com/prometheus/common/expfmt"
 	"github.com/prometheus/common/model"
 	"github.com/prometheus/common/model"
-	"golang.org/x/net/context"
 
 
 	dto "github.com/prometheus/client_model/go"
 	dto "github.com/prometheus/client_model/go"
 
 

+ 1 - 0
pkg/metrics/metrics.go

@@ -379,6 +379,7 @@ func sendUsageStats() {
 	metrics["stats.alerts.count"] = statsQuery.Result.Alerts
 	metrics["stats.alerts.count"] = statsQuery.Result.Alerts
 	metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
 	metrics["stats.active_users.count"] = statsQuery.Result.ActiveUsers
 	metrics["stats.datasources.count"] = statsQuery.Result.Datasources
 	metrics["stats.datasources.count"] = statsQuery.Result.Datasources
+	metrics["stats.stars.count"] = statsQuery.Result.Stars
 
 
 	dsStats := models.GetDataSourceStatsQuery{}
 	dsStats := models.GetDataSourceStatsQuery{}
 	if err := bus.Dispatch(&dsStats); err != nil {
 	if err := bus.Dispatch(&dsStats); err != nil {

+ 1 - 0
pkg/models/stats.go

@@ -8,6 +8,7 @@ type SystemStats struct {
 	Orgs        int64
 	Orgs        int64
 	Playlists   int64
 	Playlists   int64
 	Alerts      int64
 	Alerts      int64
+	Stars       int64
 }
 }
 
 
 type DataSourceStats struct {
 type DataSourceStats struct {

+ 0 - 150
pkg/plugins/datasource/tsdb/datasource_plugin_wrapper.go

@@ -1,150 +0,0 @@
-package tsdb
-
-import (
-	"fmt"
-	"github.com/grafana/grafana/pkg/components/null"
-	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/tsdb"
-	proto "github.com/grafana/grafana/pkg/tsdb/models"
-	"golang.org/x/net/context"
-)
-
-func NewDatasourcePluginWrapper(log log.Logger, plugin TsdbPlugin) *DatasourcePluginWrapper {
-	return &DatasourcePluginWrapper{TsdbPlugin: plugin, logger: log}
-}
-
-type DatasourcePluginWrapper struct {
-	TsdbPlugin
-
-	logger log.Logger
-}
-
-func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
-	jsonData, err := ds.JsonData.MarshalJSON()
-	if err != nil {
-		return nil, err
-	}
-
-	pbQuery := &proto.TsdbQuery{
-		Datasource: &proto.DatasourceInfo{
-			JsonData: string(jsonData),
-			Name:     ds.Name,
-			Type:     ds.Type,
-			Url:      ds.Url,
-			Id:       ds.Id,
-			OrgId:    ds.OrgId,
-		},
-		TimeRange: &proto.TimeRange{
-			FromRaw:     query.TimeRange.From,
-			ToRaw:       query.TimeRange.To,
-			ToEpochMs:   query.TimeRange.GetToAsMsEpoch(),
-			FromEpochMs: query.TimeRange.GetFromAsMsEpoch(),
-		},
-		Queries: []*proto.Query{},
-	}
-
-	for _, q := range query.Queries {
-		modelJson, _ := q.Model.MarshalJSON()
-
-		pbQuery.Queries = append(pbQuery.Queries, &proto.Query{
-			ModelJson:     string(modelJson),
-			IntervalMs:    q.IntervalMs,
-			RefId:         q.RefId,
-			MaxDataPoints: q.MaxDataPoints,
-		})
-	}
-
-	pbres, err := tw.TsdbPlugin.Query(ctx, pbQuery)
-
-	if err != nil {
-		return nil, err
-	}
-
-	res := &tsdb.Response{
-		Results: map[string]*tsdb.QueryResult{},
-	}
-
-	for _, r := range pbres.Results {
-		res.Results[r.RefId] = &tsdb.QueryResult{
-			RefId:  r.RefId,
-			Series: []*tsdb.TimeSeries{},
-		}
-
-		for _, s := range r.GetSeries() {
-			points := tsdb.TimeSeriesPoints{}
-
-			for _, p := range s.Points {
-				po := tsdb.NewTimePoint(null.FloatFrom(p.Value), float64(p.Timestamp))
-				points = append(points, po)
-			}
-
-			res.Results[r.RefId].Series = append(res.Results[r.RefId].Series, &tsdb.TimeSeries{
-				Name:   s.Name,
-				Tags:   s.Tags,
-				Points: points,
-			})
-		}
-
-		mappedTables, err := tw.mapTables(r)
-		if err != nil {
-			return nil, err
-		}
-		res.Results[r.RefId].Tables = mappedTables
-	}
-
-	return res, nil
-}
-func (tw *DatasourcePluginWrapper) mapTables(r *proto.QueryResult) ([]*tsdb.Table, error) {
-	var tables []*tsdb.Table
-	for _, t := range r.GetTables() {
-		mappedTable, err := tw.mapTable(t)
-		if err != nil {
-			return nil, err
-		}
-		tables = append(tables, mappedTable)
-	}
-	return tables, nil
-}
-
-func (tw *DatasourcePluginWrapper) mapTable(t *proto.Table) (*tsdb.Table, error) {
-	table := &tsdb.Table{}
-	for _, c := range t.GetColumns() {
-		table.Columns = append(table.Columns, tsdb.TableColumn{
-			Text: c.Name,
-		})
-	}
-
-	for _, r := range t.GetRows() {
-		row := tsdb.RowValues{}
-		for _, rv := range r.Values {
-			mappedRw, err := tw.mapRowValue(rv)
-			if err != nil {
-				return nil, err
-			}
-
-			row = append(row, mappedRw)
-		}
-		table.Rows = append(table.Rows, row)
-	}
-
-	return table, nil
-}
-func (tw *DatasourcePluginWrapper) mapRowValue(rv *proto.RowValue) (interface{}, error) {
-	switch rv.Kind {
-	case proto.RowValue_TYPE_NULL:
-		return nil, nil
-	case proto.RowValue_TYPE_INT64:
-		return rv.Int64Value, nil
-	case proto.RowValue_TYPE_BOOL:
-		return rv.BoolValue, nil
-	case proto.RowValue_TYPE_STRING:
-		return rv.StringValue, nil
-	case proto.RowValue_TYPE_DOUBLE:
-		return rv.DoubleValue, nil
-	case proto.RowValue_TYPE_BYTES:
-		return rv.BytesValue, nil
-	default:
-		return nil, fmt.Errorf("Unsupported row value %v from plugin", rv.Kind)
-	}
-}

+ 0 - 108
pkg/plugins/datasource/tsdb/datasource_plugin_wrapper_test.go

@@ -1,108 +0,0 @@
-package tsdb
-
-import (
-	"github.com/grafana/grafana/pkg/log"
-	"github.com/grafana/grafana/pkg/tsdb"
-	"github.com/grafana/grafana/pkg/tsdb/models"
-	"testing"
-)
-
-func TestMapTables(t *testing.T) {
-	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
-	var qr = &proto.QueryResult{}
-	qr.Tables = append(qr.Tables, &proto.Table{
-		Columns: []*proto.TableColumn{},
-		Rows:    nil,
-	})
-	want := []*tsdb.Table{{}}
-
-	have, err := dpw.mapTables(qr)
-	if err != nil {
-		t.Errorf("failed to map tables. error: %v", err)
-	}
-	if len(want) != len(have) {
-		t.Errorf("could not map all tables")
-	}
-}
-
-func TestMapTable(t *testing.T) {
-	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
-
-	source := &proto.Table{
-		Columns: []*proto.TableColumn{{Name: "column1"}, {Name: "column2"}},
-		Rows: []*proto.TableRow{{
-			Values: []*proto.RowValue{
-				{
-					Kind:      proto.RowValue_TYPE_BOOL,
-					BoolValue: true,
-				},
-				{
-					Kind:       proto.RowValue_TYPE_INT64,
-					Int64Value: 42,
-				},
-			},
-		}},
-	}
-
-	want := &tsdb.Table{
-		Columns: []tsdb.TableColumn{{Text: "column1"}, {Text: "column2"}},
-	}
-	have, err := dpw.mapTable(source)
-	if err != nil {
-		t.Fatalf("failed to map table. error: %v", err)
-	}
-
-	for i := range have.Columns {
-		if want.Columns[i] != have.Columns[i] {
-			t.Fatalf("have column: %s, want %s", have, want)
-		}
-	}
-
-	if len(have.Rows) != 1 {
-		t.Fatalf("Expects one row but got %d", len(have.Rows))
-	}
-
-	rowValuesCount := len(have.Rows[0])
-	if rowValuesCount != 2 {
-		t.Fatalf("Expects two row values, got %d", rowValuesCount)
-	}
-}
-
-func TestMappingRowValue(t *testing.T) {
-	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
-
-	boolRowValue, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_BOOL, BoolValue: true})
-	haveBool, ok := boolRowValue.(bool)
-	if !ok || haveBool != true {
-		t.Fatalf("Expected true, was %s", haveBool)
-	}
-
-	intRowValue, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_INT64, Int64Value: 42})
-	haveInt, ok := intRowValue.(int64)
-	if !ok || haveInt != 42 {
-		t.Fatalf("Expected %d, was %d", 42, haveInt)
-	}
-
-	stringRowValue, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_STRING, StringValue: "grafana"})
-	haveString, ok := stringRowValue.(string)
-	if !ok || haveString != "grafana" {
-		t.Fatalf("Expected %s, was %s", "grafana", haveString)
-	}
-
-	doubleRowValue, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_DOUBLE, DoubleValue: 1.5})
-	haveDouble, ok := doubleRowValue.(float64)
-	if !ok || haveDouble != 1.5 {
-		t.Fatalf("Expected %v, was %v", 1.5, haveDouble)
-	}
-
-	bytesRowValue, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_BYTES, BytesValue: []byte{66}})
-	haveBytes, ok := bytesRowValue.([]byte)
-	if !ok || len(haveBytes) != 1 || haveBytes[0] != 66 {
-		t.Fatalf("Expected %v, was %v", []byte{66}, haveBytes)
-	}
-
-	haveNil, _ := dpw.mapRowValue(&proto.RowValue{Kind: proto.RowValue_TYPE_NULL})
-	if haveNil != nil {
-		t.Fatalf("Expected %v, was %v", nil, haveNil)
-	}
-}

+ 0 - 22
pkg/plugins/datasource/tsdb/grpc.go

@@ -1,22 +0,0 @@
-package tsdb
-
-import (
-	proto "github.com/grafana/grafana/pkg/tsdb/models"
-	"golang.org/x/net/context"
-)
-
-type GRPCClient struct {
-	proto.TsdbPluginClient
-}
-
-func (m *GRPCClient) Query(ctx context.Context, req *proto.TsdbQuery) (*proto.Response, error) {
-	return m.TsdbPluginClient.Query(ctx, req)
-}
-
-type GRPCServer struct {
-	TsdbPlugin
-}
-
-func (m *GRPCServer) Query(ctx context.Context, req *proto.TsdbQuery) (*proto.Response, error) {
-	return m.TsdbPlugin.Query(ctx, req)
-}

+ 0 - 27
pkg/plugins/datasource/tsdb/interface.go

@@ -1,27 +0,0 @@
-package tsdb
-
-import (
-	"golang.org/x/net/context"
-
-	proto "github.com/grafana/grafana/pkg/tsdb/models"
-	plugin "github.com/hashicorp/go-plugin"
-	"google.golang.org/grpc"
-)
-
-type TsdbPlugin interface {
-	Query(ctx context.Context, req *proto.TsdbQuery) (*proto.Response, error)
-}
-
-type TsdbPluginImpl struct {
-	plugin.NetRPCUnsupportedPlugin
-	Plugin TsdbPlugin
-}
-
-func (p *TsdbPluginImpl) GRPCServer(s *grpc.Server) error {
-	proto.RegisterTsdbPluginServer(s, &GRPCServer{p.Plugin})
-	return nil
-}
-
-func (p *TsdbPluginImpl) GRPCClient(c *grpc.ClientConn) (interface{}, error) {
-	return &GRPCClient{proto.NewTsdbPluginClient(c)}, nil
-}

+ 156 - 0
pkg/plugins/datasource/wrapper/datasource_plugin_wrapper.go

@@ -0,0 +1,156 @@
+package wrapper
+
+import (
+	"context"
+	"errors"
+	"fmt"
+
+	"github.com/grafana/grafana/pkg/components/null"
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana_plugin_model/go/datasource"
+)
+
+func NewDatasourcePluginWrapper(log log.Logger, plugin datasource.DatasourcePlugin) *DatasourcePluginWrapper {
+	return &DatasourcePluginWrapper{DatasourcePlugin: plugin, logger: log}
+}
+
+type DatasourcePluginWrapper struct {
+	datasource.DatasourcePlugin
+	logger log.Logger
+}
+
+func (tw *DatasourcePluginWrapper) Query(ctx context.Context, ds *models.DataSource, query *tsdb.TsdbQuery) (*tsdb.Response, error) {
+	jsonData, err := ds.JsonData.MarshalJSON()
+	if err != nil {
+		return nil, err
+	}
+
+	pbQuery := &datasource.DatasourceRequest{
+		Datasource: &datasource.DatasourceInfo{
+			Name:                    ds.Name,
+			Type:                    ds.Type,
+			Url:                     ds.Url,
+			Id:                      ds.Id,
+			OrgId:                   ds.OrgId,
+			JsonData:                string(jsonData),
+			DecryptedSecureJsonData: ds.SecureJsonData.Decrypt(),
+		},
+		TimeRange: &datasource.TimeRange{
+			FromRaw:     query.TimeRange.From,
+			ToRaw:       query.TimeRange.To,
+			ToEpochMs:   query.TimeRange.GetToAsMsEpoch(),
+			FromEpochMs: query.TimeRange.GetFromAsMsEpoch(),
+		},
+		Queries: []*datasource.Query{},
+	}
+
+	for _, q := range query.Queries {
+		modelJson, _ := q.Model.MarshalJSON()
+
+		pbQuery.Queries = append(pbQuery.Queries, &datasource.Query{
+			ModelJson:     string(modelJson),
+			IntervalMs:    q.IntervalMs,
+			RefId:         q.RefId,
+			MaxDataPoints: q.MaxDataPoints,
+		})
+	}
+
+	pbres, err := tw.DatasourcePlugin.Query(ctx, pbQuery)
+
+	if err != nil {
+		return nil, err
+	}
+
+	res := &tsdb.Response{
+		Results: map[string]*tsdb.QueryResult{},
+	}
+
+	for _, r := range pbres.Results {
+		qr := &tsdb.QueryResult{
+			RefId:       r.RefId,
+			Series:      []*tsdb.TimeSeries{},
+			Error:       errors.New(r.Error),
+			ErrorString: r.Error,
+		}
+
+		for _, s := range r.GetSeries() {
+			points := tsdb.TimeSeriesPoints{}
+
+			for _, p := range s.Points {
+				po := tsdb.NewTimePoint(null.FloatFrom(p.Value), float64(p.Timestamp))
+				points = append(points, po)
+			}
+
+			qr.Series = append(qr.Series, &tsdb.TimeSeries{
+				Name:   s.Name,
+				Tags:   s.Tags,
+				Points: points,
+			})
+		}
+
+		mappedTables, err := tw.mapTables(r)
+		if err != nil {
+			return nil, err
+		}
+		qr.Tables = mappedTables
+
+		res.Results[r.RefId] = qr
+	}
+
+	return res, nil
+}
+func (tw *DatasourcePluginWrapper) mapTables(r *datasource.QueryResult) ([]*tsdb.Table, error) {
+	var tables []*tsdb.Table
+	for _, t := range r.GetTables() {
+		mappedTable, err := tw.mapTable(t)
+		if err != nil {
+			return nil, err
+		}
+		tables = append(tables, mappedTable)
+	}
+	return tables, nil
+}
+
+func (tw *DatasourcePluginWrapper) mapTable(t *datasource.Table) (*tsdb.Table, error) {
+	table := &tsdb.Table{}
+	for _, c := range t.GetColumns() {
+		table.Columns = append(table.Columns, tsdb.TableColumn{
+			Text: c.Name,
+		})
+	}
+
+	for _, r := range t.GetRows() {
+		row := tsdb.RowValues{}
+		for _, rv := range r.Values {
+			mappedRw, err := tw.mapRowValue(rv)
+			if err != nil {
+				return nil, err
+			}
+
+			row = append(row, mappedRw)
+		}
+		table.Rows = append(table.Rows, row)
+	}
+
+	return table, nil
+}
+func (tw *DatasourcePluginWrapper) mapRowValue(rv *datasource.RowValue) (interface{}, error) {
+	switch rv.Kind {
+	case datasource.RowValue_TYPE_NULL:
+		return nil, nil
+	case datasource.RowValue_TYPE_INT64:
+		return rv.Int64Value, nil
+	case datasource.RowValue_TYPE_BOOL:
+		return rv.BoolValue, nil
+	case datasource.RowValue_TYPE_STRING:
+		return rv.StringValue, nil
+	case datasource.RowValue_TYPE_DOUBLE:
+		return rv.DoubleValue, nil
+	case datasource.RowValue_TYPE_BYTES:
+		return rv.BytesValue, nil
+	default:
+		return nil, fmt.Errorf("Unsupported row value %v from plugin", rv.Kind)
+	}
+}

+ 109 - 0
pkg/plugins/datasource/wrapper/datasource_plugin_wrapper_test.go

@@ -0,0 +1,109 @@
+package wrapper
+
+import (
+	"testing"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana_plugin_model/go/datasource"
+)
+
+func TestMapTables(t *testing.T) {
+	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
+	var qr = &datasource.QueryResult{}
+	qr.Tables = append(qr.Tables, &datasource.Table{
+		Columns: []*datasource.TableColumn{},
+		Rows:    nil,
+	})
+	want := []*tsdb.Table{{}}
+
+	have, err := dpw.mapTables(qr)
+	if err != nil {
+		t.Errorf("failed to map tables. error: %v", err)
+	}
+	if len(want) != len(have) {
+		t.Errorf("could not map all tables")
+	}
+}
+
+func TestMapTable(t *testing.T) {
+	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
+
+	source := &datasource.Table{
+		Columns: []*datasource.TableColumn{{Name: "column1"}, {Name: "column2"}},
+		Rows: []*datasource.TableRow{{
+			Values: []*datasource.RowValue{
+				{
+					Kind:      datasource.RowValue_TYPE_BOOL,
+					BoolValue: true,
+				},
+				{
+					Kind:       datasource.RowValue_TYPE_INT64,
+					Int64Value: 42,
+				},
+			},
+		}},
+	}
+
+	want := &tsdb.Table{
+		Columns: []tsdb.TableColumn{{Text: "column1"}, {Text: "column2"}},
+	}
+	have, err := dpw.mapTable(source)
+	if err != nil {
+		t.Fatalf("failed to map table. error: %v", err)
+	}
+
+	for i := range have.Columns {
+		if want.Columns[i] != have.Columns[i] {
+			t.Fatalf("have column: %s, want %s", have, want)
+		}
+	}
+
+	if len(have.Rows) != 1 {
+		t.Fatalf("Expects one row but got %d", len(have.Rows))
+	}
+
+	rowValuesCount := len(have.Rows[0])
+	if rowValuesCount != 2 {
+		t.Fatalf("Expects two row values, got %d", rowValuesCount)
+	}
+}
+
+func TestMappingRowValue(t *testing.T) {
+	dpw := NewDatasourcePluginWrapper(log.New("test-logger"), nil)
+
+	boolRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BOOL, BoolValue: true})
+	haveBool, ok := boolRowValue.(bool)
+	if !ok || haveBool != true {
+		t.Fatalf("Expected true, was %s", haveBool)
+	}
+
+	intRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_INT64, Int64Value: 42})
+	haveInt, ok := intRowValue.(int64)
+	if !ok || haveInt != 42 {
+		t.Fatalf("Expected %d, was %d", 42, haveInt)
+	}
+
+	stringRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_STRING, StringValue: "grafana"})
+	haveString, ok := stringRowValue.(string)
+	if !ok || haveString != "grafana" {
+		t.Fatalf("Expected %s, was %s", "grafana", haveString)
+	}
+
+	doubleRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_DOUBLE, DoubleValue: 1.5})
+	haveDouble, ok := doubleRowValue.(float64)
+	if !ok || haveDouble != 1.5 {
+		t.Fatalf("Expected %v, was %v", 1.5, haveDouble)
+	}
+
+	bytesRowValue, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_BYTES, BytesValue: []byte{66}})
+	haveBytes, ok := bytesRowValue.([]byte)
+	if !ok || len(haveBytes) != 1 || haveBytes[0] != 66 {
+		t.Fatalf("Expected %v, was %v", []byte{66}, haveBytes)
+	}
+
+	haveNil, _ := dpw.mapRowValue(&datasource.RowValue{Kind: datasource.RowValue_TYPE_NULL})
+	if haveNil != nil {
+		t.Fatalf("Expected %v, was %v", nil, haveNil)
+	}
+}

+ 5 - 4
pkg/plugins/datasource_plugin.go

@@ -14,8 +14,9 @@ import (
 
 
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
-	shared "github.com/grafana/grafana/pkg/plugins/datasource/tsdb"
+	"github.com/grafana/grafana/pkg/plugins/datasource/wrapper"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
+	"github.com/grafana/grafana_plugin_model/go/datasource"
 	plugin "github.com/hashicorp/go-plugin"
 	plugin "github.com/hashicorp/go-plugin"
 )
 )
 
 
@@ -92,7 +93,7 @@ func (p *DataSourcePlugin) spawnSubProcess() error {
 
 
 	p.client = plugin.NewClient(&plugin.ClientConfig{
 	p.client = plugin.NewClient(&plugin.ClientConfig{
 		HandshakeConfig:  handshakeConfig,
 		HandshakeConfig:  handshakeConfig,
-		Plugins:          map[string]plugin.Plugin{p.Id: &shared.TsdbPluginImpl{}},
+		Plugins:          map[string]plugin.Plugin{p.Id: &datasource.DatasourcePluginImpl{}},
 		Cmd:              exec.Command(fullpath),
 		Cmd:              exec.Command(fullpath),
 		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
 		AllowedProtocols: []plugin.Protocol{plugin.ProtocolGRPC},
 		Logger:           LogWrapper{Logger: p.log},
 		Logger:           LogWrapper{Logger: p.log},
@@ -108,10 +109,10 @@ func (p *DataSourcePlugin) spawnSubProcess() error {
 		return err
 		return err
 	}
 	}
 
 
-	plugin := raw.(shared.TsdbPlugin)
+	plugin := raw.(datasource.DatasourcePlugin)
 
 
 	tsdb.RegisterTsdbQueryEndpoint(p.Id, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
 	tsdb.RegisterTsdbQueryEndpoint(p.Id, func(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
-		return shared.NewDatasourcePluginWrapper(p.log, plugin), nil
+		return wrapper.NewDatasourcePluginWrapper(p.log, plugin), nil
 	})
 	})
 
 
 	return nil
 	return nil

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

@@ -20,7 +20,7 @@ func init() {
 		OptionsTemplate: `
 		OptionsTemplate: `
       <h3 class="page-heading">Email addresses</h3>
       <h3 class="page-heading">Email addresses</h3>
       <div class="gf-form">
       <div class="gf-form">
-         <textarea rows="7" class="gf-form-input width-25" required ng-model="ctrl.model.settings.addresses"></textarea>
+         <textarea rows="7" class="gf-form-input width-27" required ng-model="ctrl.model.settings.addresses"></textarea>
       </div>
       </div>
       <div class="gf-form">
       <div class="gf-form">
       <span>You can enter multiple email addresses using a ";" separator</span>
       <span>You can enter multiple email addresses using a ";" separator</span>

+ 12 - 2
pkg/services/alerting/notifiers/opsgenie.go

@@ -23,6 +23,10 @@ func init() {
         <span class="gf-form-label width-14">API Key</span>
         <span class="gf-form-label width-14">API Key</span>
         <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
         <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiKey" placeholder="OpsGenie API Key"></input>
       </div>
       </div>
+      <div class="gf-form">
+        <span class="gf-form-label width-14">Alert API Url</span>
+        <input type="text" required class="gf-form-input max-width-22" ng-model="ctrl.model.settings.apiUrl" placeholder="https://api.opsgenie.com/v2/alerts"></input>
+      </div>
       <div class="gf-form">
       <div class="gf-form">
         <gf-form-switch
         <gf-form-switch
            class="gf-form"
            class="gf-form"
@@ -43,13 +47,18 @@ var (
 func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
 	autoClose := model.Settings.Get("autoClose").MustBool(true)
 	autoClose := model.Settings.Get("autoClose").MustBool(true)
 	apiKey := model.Settings.Get("apiKey").MustString()
 	apiKey := model.Settings.Get("apiKey").MustString()
+	apiUrl := model.Settings.Get("apiUrl").MustString()
 	if apiKey == "" {
 	if apiKey == "" {
 		return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
 		return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"}
 	}
 	}
+	if apiUrl == "" {
+		apiUrl = opsgenieAlertURL
+	}
 
 
 	return &OpsGenieNotifier{
 	return &OpsGenieNotifier{
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings),
 		ApiKey:       apiKey,
 		ApiKey:       apiKey,
+		ApiUrl:       apiUrl,
 		AutoClose:    autoClose,
 		AutoClose:    autoClose,
 		log:          log.New("alerting.notifier.opsgenie"),
 		log:          log.New("alerting.notifier.opsgenie"),
 	}, nil
 	}, nil
@@ -58,6 +67,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error)
 type OpsGenieNotifier struct {
 type OpsGenieNotifier struct {
 	NotifierBase
 	NotifierBase
 	ApiKey    string
 	ApiKey    string
+	ApiUrl    string
 	AutoClose bool
 	AutoClose bool
 	log       log.Logger
 	log       log.Logger
 }
 }
@@ -105,7 +115,7 @@ func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) err
 	body, _ := bodyJSON.MarshalJSON()
 	body, _ := bodyJSON.MarshalJSON()
 
 
 	cmd := &m.SendWebhookSync{
 	cmd := &m.SendWebhookSync{
-		Url:        opsgenieAlertURL,
+		Url:        this.ApiUrl,
 		Body:       string(body),
 		Body:       string(body),
 		HttpMethod: "POST",
 		HttpMethod: "POST",
 		HttpHeader: map[string]string{
 		HttpHeader: map[string]string{
@@ -129,7 +139,7 @@ func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) erro
 	body, _ := bodyJSON.MarshalJSON()
 	body, _ := bodyJSON.MarshalJSON()
 
 
 	cmd := &m.SendWebhookSync{
 	cmd := &m.SendWebhookSync{
-		Url:        fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", opsgenieAlertURL, evalContext.Rule.Id),
+		Url:        fmt.Sprintf("%s/alertId-%d/close?identifierType=alias", this.ApiUrl, evalContext.Rule.Id),
 		Body:       string(body),
 		Body:       string(body),
 		HttpMethod: "POST",
 		HttpMethod: "POST",
 		HttpHeader: map[string]string{
 		HttpHeader: map[string]string{

+ 10 - 5
pkg/services/provisioning/dashboards/config_reader.go

@@ -5,20 +5,25 @@ import (
 	"path/filepath"
 	"path/filepath"
 	"strings"
 	"strings"
 
 
+	"github.com/grafana/grafana/pkg/log"
 	yaml "gopkg.in/yaml.v2"
 	yaml "gopkg.in/yaml.v2"
 )
 )
 
 
 type configReader struct {
 type configReader struct {
 	path string
 	path string
+	log  log.Logger
 }
 }
 
 
 func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
+	var dashboards []*DashboardsAsConfig
+
 	files, err := ioutil.ReadDir(cr.path)
 	files, err := ioutil.ReadDir(cr.path)
+
 	if err != nil {
 	if err != nil {
-		return nil, err
+		cr.log.Error("cant read dashboard provisioning files from directory", "path", cr.path)
+		return dashboards, nil
 	}
 	}
 
 
-	var dashboards []*DashboardsAsConfig
 	for _, file := range files {
 	for _, file := range files {
 		if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") {
 		if !strings.HasSuffix(file.Name(), ".yaml") && !strings.HasSuffix(file.Name(), ".yml") {
 			continue
 			continue
@@ -30,13 +35,13 @@ func (cr *configReader) readConfig() ([]*DashboardsAsConfig, error) {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
-		var datasource []*DashboardsAsConfig
-		err = yaml.Unmarshal(yamlFile, &datasource)
+		var dashCfg []*DashboardsAsConfig
+		err = yaml.Unmarshal(yamlFile, &dashCfg)
 		if err != nil {
 		if err != nil {
 			return nil, err
 			return nil, err
 		}
 		}
 
 
-		dashboards = append(dashboards, datasource...)
+		dashboards = append(dashboards, dashCfg...)
 	}
 	}
 
 
 	for i := range dashboards {
 	for i := range dashboards {

+ 18 - 7
pkg/services/provisioning/dashboards/config_reader_test.go

@@ -3,6 +3,7 @@ package dashboards
 import (
 import (
 	"testing"
 	"testing"
 
 
+	"github.com/grafana/grafana/pkg/log"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
 )
 )
 
 
@@ -16,8 +17,8 @@ func TestDashboardsAsConfig(t *testing.T) {
 
 
 		Convey("Can read config file", func() {
 		Convey("Can read config file", func() {
 
 
-			cfgProvifer := configReader{path: simpleDashboardConfig}
-			cfg, err := cfgProvifer.readConfig()
+			cfgProvider := configReader{path: simpleDashboardConfig, log: log.New("test-logger")}
+			cfg, err := cfgProvider.readConfig()
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)
 			}
 			}
@@ -33,7 +34,7 @@ func TestDashboardsAsConfig(t *testing.T) {
 			So(ds.Editable, ShouldBeTrue)
 			So(ds.Editable, ShouldBeTrue)
 
 
 			So(len(ds.Options), ShouldEqual, 1)
 			So(len(ds.Options), ShouldEqual, 1)
-			So(ds.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards")
+			So(ds.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
 
 
 			ds2 := cfg[1]
 			ds2 := cfg[1]
 
 
@@ -44,19 +45,29 @@ func TestDashboardsAsConfig(t *testing.T) {
 			So(ds2.Editable, ShouldBeFalse)
 			So(ds2.Editable, ShouldBeFalse)
 
 
 			So(len(ds2.Options), ShouldEqual, 1)
 			So(len(ds2.Options), ShouldEqual, 1)
-			So(ds2.Options["folder"], ShouldEqual, "/var/lib/grafana/dashboards")
+			So(ds2.Options["path"], ShouldEqual, "/var/lib/grafana/dashboards")
 		})
 		})
 
 
-		Convey("Should skip broken config files", func() {
+		Convey("Should skip invalid path", func() {
 
 
-			cfgProvifer := configReader{path: brokenConfigs}
-			cfg, err := cfgProvifer.readConfig()
+			cfgProvider := configReader{path: "/invalid-directory", log: log.New("test-logger")}
+			cfg, err := cfgProvider.readConfig()
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)
 			}
 			}
 
 
 			So(len(cfg), ShouldEqual, 0)
 			So(len(cfg), ShouldEqual, 0)
+		})
 
 
+		Convey("Should skip broken config files", func() {
+
+			cfgProvider := configReader{path: brokenConfigs, log: log.New("test-logger")}
+			cfg, err := cfgProvider.readConfig()
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+
+			So(len(cfg), ShouldEqual, 0)
 		})
 		})
 	})
 	})
 }
 }

+ 3 - 2
pkg/services/provisioning/dashboards/dashboard.go

@@ -14,9 +14,10 @@ type DashboardProvisioner struct {
 }
 }
 
 
 func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
 func Provision(ctx context.Context, configDirectory string) (*DashboardProvisioner, error) {
+	log := log.New("provisioning.dashboard")
 	d := &DashboardProvisioner{
 	d := &DashboardProvisioner{
-		cfgReader: &configReader{path: configDirectory},
-		log:       log.New("provisioning.dashboard"),
+		cfgReader: &configReader{path: configDirectory, log: log},
+		log:       log,
 		ctx:       ctx,
 		ctx:       ctx,
 	}
 	}
 
 

+ 19 - 2
pkg/services/provisioning/dashboards/file_reader.go

@@ -34,9 +34,15 @@ type fileReader struct {
 }
 }
 
 
 func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
 func NewDashboardFileReader(cfg *DashboardsAsConfig, log log.Logger) (*fileReader, error) {
-	path, ok := cfg.Options["folder"].(string)
+	var path string
+	path, ok := cfg.Options["path"].(string)
 	if !ok {
 	if !ok {
-		return nil, fmt.Errorf("Failed to load dashboards. folder param is not a string")
+		path, ok = cfg.Options["folder"].(string)
+		if !ok {
+			return nil, fmt.Errorf("Failed to load dashboards. path param is not a string")
+		}
+
+		log.Warn("[Deprecated] The folder property is deprecated. Please use path instead.")
 	}
 	}
 
 
 	if _, err := os.Stat(path); os.IsNotExist(err) {
 	if _, err := os.Stat(path); os.IsNotExist(err) {
@@ -145,6 +151,17 @@ func createWalkFn(fr *fileReader, folderId int64) filepath.WalkFunc {
 			return nil
 			return nil
 		}
 		}
 
 
+		checkFilepath, err := filepath.EvalSymlinks(path)
+
+		if path != checkFilepath {
+			path = checkFilepath
+			fi, err := os.Lstat(checkFilepath)
+			if err != nil {
+				return err
+			}
+			fileInfo = fi
+		}
+
 		cachedDashboard, exist := fr.cache.getCache(path)
 		cachedDashboard, exist := fr.cache.getCache(path)
 		if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
 		if exist && cachedDashboard.UpdatedAt == fileInfo.ModTime() {
 			return nil
 			return nil

+ 29 - 5
pkg/services/provisioning/dashboards/file_reader_test.go

@@ -42,7 +42,7 @@ func TestDashboardFileReader(t *testing.T) {
 			}
 			}
 
 
 			Convey("Can read default dashboard", func() {
 			Convey("Can read default dashboard", func() {
-				cfg.Options["folder"] = defaultDashboards
+				cfg.Options["path"] = defaultDashboards
 				cfg.Folder = "Team A"
 				cfg.Folder = "Team A"
 
 
 				reader, err := NewDashboardFileReader(cfg, logger)
 				reader, err := NewDashboardFileReader(cfg, logger)
@@ -67,7 +67,7 @@ func TestDashboardFileReader(t *testing.T) {
 			})
 			})
 
 
 			Convey("Should not update dashboards when db is newer", func() {
 			Convey("Should not update dashboards when db is newer", func() {
-				cfg.Options["folder"] = oneDashboard
+				cfg.Options["path"] = oneDashboard
 
 
 				fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
 				fakeRepo.getDashboard = append(fakeRepo.getDashboard, &models.Dashboard{
 					Updated: time.Now().Add(time.Hour),
 					Updated: time.Now().Add(time.Hour),
@@ -84,7 +84,7 @@ func TestDashboardFileReader(t *testing.T) {
 			})
 			})
 
 
 			Convey("Can read default dashboard and replace old version in database", func() {
 			Convey("Can read default dashboard and replace old version in database", func() {
-				cfg.Options["folder"] = oneDashboard
+				cfg.Options["path"] = oneDashboard
 
 
 				stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
 				stat, _ := os.Stat(oneDashboard + "/dashboard1.json")
 
 
@@ -115,7 +115,7 @@ func TestDashboardFileReader(t *testing.T) {
 			})
 			})
 
 
 			Convey("Broken dashboards should not cause error", func() {
 			Convey("Broken dashboards should not cause error", func() {
-				cfg.Options["folder"] = brokenDashboards
+				cfg.Options["path"] = brokenDashboards
 
 
 				_, err := NewDashboardFileReader(cfg, logger)
 				_, err := NewDashboardFileReader(cfg, logger)
 				So(err, ShouldBeNil)
 				So(err, ShouldBeNil)
@@ -167,7 +167,7 @@ func TestDashboardFileReader(t *testing.T) {
 				OrgId:  1,
 				OrgId:  1,
 				Folder: "",
 				Folder: "",
 				Options: map[string]interface{}{
 				Options: map[string]interface{}{
-					"folder": defaultDashboards,
+					"path": defaultDashboards,
 				},
 				},
 			}
 			}
 
 
@@ -184,6 +184,30 @@ func TestDashboardFileReader(t *testing.T) {
 				So(shouldSkip, ShouldBeNil)
 				So(shouldSkip, ShouldBeNil)
 			})
 			})
 		})
 		})
+
+		Convey("Can use bpth path and folder as dashboard path", func() {
+			cfg := &DashboardsAsConfig{
+				Name:    "Default",
+				Type:    "file",
+				OrgId:   1,
+				Folder:  "",
+				Options: map[string]interface{}{},
+			}
+
+			Convey("using path parameter", func() {
+				cfg.Options["path"] = defaultDashboards
+				reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
+				So(err, ShouldBeNil)
+				So(reader.Path, ShouldEqual, defaultDashboards)
+			})
+
+			Convey("using folder as options", func() {
+				cfg.Options["folder"] = defaultDashboards
+				reader, err := NewDashboardFileReader(cfg, log.New("test-logger"))
+				So(err, ShouldBeNil)
+				So(reader.Path, ShouldEqual, defaultDashboards)
+			})
+		})
 	})
 	})
 }
 }
 
 

+ 2 - 2
pkg/services/provisioning/dashboards/test-configs/dashboards-from-disk/dev-dashboards.yaml

@@ -4,9 +4,9 @@
   editable: true
   editable: true
   type: file
   type: file
   options:
   options:
-    folder: /var/lib/grafana/dashboards
+    path: /var/lib/grafana/dashboards
 
 
 - name: 'default'
 - name: 'default'
   type: file
   type: file
   options:
   options:
-    folder: /var/lib/grafana/dashboards
+    path: /var/lib/grafana/dashboards

+ 10 - 6
pkg/services/provisioning/datasources/datasources.go

@@ -25,13 +25,13 @@ func Provision(configDirectory string) error {
 
 
 type DatasourceProvisioner struct {
 type DatasourceProvisioner struct {
 	log         log.Logger
 	log         log.Logger
-	cfgProvider configReader
+	cfgProvider *configReader
 }
 }
 
 
 func newDatasourceProvisioner(log log.Logger) DatasourceProvisioner {
 func newDatasourceProvisioner(log log.Logger) DatasourceProvisioner {
 	return DatasourceProvisioner{
 	return DatasourceProvisioner{
 		log:         log,
 		log:         log,
-		cfgProvider: configReader{},
+		cfgProvider: &configReader{log: log},
 	}
 	}
 }
 }
 
 
@@ -95,15 +95,19 @@ func (dc *DatasourceProvisioner) deleteDatasources(dsToDelete []*DeleteDatasourc
 	return nil
 	return nil
 }
 }
 
 
-type configReader struct{}
+type configReader struct {
+	log log.Logger
+}
+
+func (cr *configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
+	var datasources []*DatasourcesAsConfig
 
 
-func (configReader) readConfig(path string) ([]*DatasourcesAsConfig, error) {
 	files, err := ioutil.ReadDir(path)
 	files, err := ioutil.ReadDir(path)
 	if err != nil {
 	if err != nil {
-		return nil, err
+		cr.log.Error("cant read datasource provisioning files from directory", "path", path)
+		return datasources, nil
 	}
 	}
 
 
-	var datasources []*DatasourcesAsConfig
 	for _, file := range files {
 	for _, file := range files {
 		if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
 		if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
 			filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
 			filename, _ := filepath.Abs(filepath.Join(path, file.Name()))

+ 14 - 3
pkg/services/provisioning/datasources/datasources_test.go

@@ -11,7 +11,7 @@ import (
 )
 )
 
 
 var (
 var (
-	logger                          log.Logger = log.New("fake.logger")
+	logger                          log.Logger = log.New("fake.log")
 	oneDatasourcesConfig            string     = ""
 	oneDatasourcesConfig            string     = ""
 	twoDatasourcesConfig            string     = "./test-configs/two-datasources"
 	twoDatasourcesConfig            string     = "./test-configs/two-datasources"
 	twoDatasourcesConfigPurgeOthers string     = "./test-configs/insert-two-delete-two"
 	twoDatasourcesConfigPurgeOthers string     = "./test-configs/insert-two-delete-two"
@@ -115,12 +115,23 @@ func TestDatasourceAsConfig(t *testing.T) {
 		})
 		})
 
 
 		Convey("broken yaml should return error", func() {
 		Convey("broken yaml should return error", func() {
-			_, err := configReader{}.readConfig(brokenYaml)
+			reader := &configReader{}
+			_, err := reader.readConfig(brokenYaml)
 			So(err, ShouldNotBeNil)
 			So(err, ShouldNotBeNil)
 		})
 		})
 
 
+		Convey("skip invalid directory", func() {
+			cfgProvifer := &configReader{log: log.New("test logger")}
+			cfg, err := cfgProvifer.readConfig("./invalid-directory")
+			if err != nil {
+				t.Fatalf("readConfig return an error %v", err)
+			}
+
+			So(len(cfg), ShouldEqual, 0)
+		})
+
 		Convey("can read all properties", func() {
 		Convey("can read all properties", func() {
-			cfgProvifer := configReader{}
+			cfgProvifer := &configReader{log: log.New("test logger")}
 			cfg, err := cfgProvifer.readConfig(allProperties)
 			cfg, err := cfgProvifer.readConfig(allProperties)
 			if err != nil {
 			if err != nil {
 				t.Fatalf("readConfig return an error %v", err)
 				t.Fatalf("readConfig return an error %v", err)

+ 4 - 4
pkg/services/sqlstore/alert.go

@@ -24,7 +24,7 @@ func init() {
 
 
 func GetAlertById(query *m.GetAlertByIdQuery) error {
 func GetAlertById(query *m.GetAlertByIdQuery) error {
 	alert := m.Alert{}
 	alert := m.Alert{}
-	has, err := x.Id(query.Id).Get(&alert)
+	has, err := x.ID(query.Id).Get(&alert)
 	if !has {
 	if !has {
 		return fmt.Errorf("could not find alert")
 		return fmt.Errorf("could not find alert")
 	}
 	}
@@ -113,7 +113,7 @@ func HandleAlertsQuery(query *m.GetAlertsQuery) error {
 	}
 	}
 
 
 	alerts := make([]*m.Alert, 0)
 	alerts := make([]*m.Alert, 0)
-	if err := x.Sql(sql.String(), params...).Find(&alerts); err != nil {
+	if err := x.SQL(sql.String(), params...).Find(&alerts); err != nil {
 		return err
 		return err
 	}
 	}
 
 
@@ -260,7 +260,7 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
 			alert.ExecutionError = cmd.Error
 			alert.ExecutionError = cmd.Error
 		}
 		}
 
 
-		sess.Id(alert.Id).Update(&alert)
+		sess.ID(alert.Id).Update(&alert)
 		return nil
 		return nil
 	})
 	})
 }
 }
@@ -324,7 +324,7 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error
 	                WHERE org_id = ? AND dashboard_id = ?`
 	                WHERE org_id = ? AND dashboard_id = ?`
 
 
 	query.Result = make([]*m.AlertStateInfoDTO, 0)
 	query.Result = make([]*m.AlertStateInfoDTO, 0)
-	err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
+	err := x.SQL(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
 
 
 	return err
 	return err
 }
 }

+ 3 - 3
pkg/services/sqlstore/alert_notification.go

@@ -76,7 +76,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
 	sql.WriteString(`)`)
 	sql.WriteString(`)`)
 
 
 	results := make([]*m.AlertNotification, 0)
 	results := make([]*m.AlertNotification, 0)
-	if err := x.Sql(sql.String(), params...).Find(&results); err != nil {
+	if err := x.SQL(sql.String(), params...).Find(&results); err != nil {
 		return err
 		return err
 	}
 	}
 
 
@@ -165,7 +165,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 	return inTransaction(func(sess *DBSession) (err error) {
 	return inTransaction(func(sess *DBSession) (err error) {
 		current := m.AlertNotification{}
 		current := m.AlertNotification{}
 
 
-		if _, err = sess.Id(cmd.Id).Get(&current); err != nil {
+		if _, err = sess.ID(cmd.Id).Get(&current); err != nil {
 			return err
 			return err
 		}
 		}
 
 
@@ -187,7 +187,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
 
 
 		sess.UseBool("is_default")
 		sess.UseBool("is_default")
 
 
-		if affected, err := sess.Id(cmd.Id).Update(current); err != nil {
+		if affected, err := sess.ID(cmd.Id).Update(current); err != nil {
 			return err
 			return err
 		} else if affected == 0 {
 		} else if affected == 0 {
 			return fmt.Errorf("Could not find alert notification")
 			return fmt.Errorf("Could not find alert notification")

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

@@ -176,6 +176,7 @@ func GetDashboardAclInfoList(query *m.GetDashboardAclInfoListQuery) error {
 					folder.has_acl = ` + dialect.BooleanStr(false) + `
 					folder.has_acl = ` + dialect.BooleanStr(false) + `
 				) AND
 				) AND
 				da.dashboard_id = -1
 				da.dashboard_id = -1
+	ORDER BY 1 ASC
 	`
 	`
 
 
 	query.Result = make([]*m.DashboardAclInfoDTO, 0)
 	query.Result = make([]*m.DashboardAclInfoDTO, 0)

+ 3 - 6
pkg/services/sqlstore/playlist.go

@@ -17,15 +17,13 @@ func init() {
 }
 }
 
 
 func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
 func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
-	var err error
-
 	playlist := m.Playlist{
 	playlist := m.Playlist{
 		Name:     cmd.Name,
 		Name:     cmd.Name,
 		Interval: cmd.Interval,
 		Interval: cmd.Interval,
 		OrgId:    cmd.OrgId,
 		OrgId:    cmd.OrgId,
 	}
 	}
 
 
-	_, err = x.Insert(&playlist)
+	_, err := x.Insert(&playlist)
 
 
 	fmt.Printf("%v", playlist.Id)
 	fmt.Printf("%v", playlist.Id)
 
 
@@ -47,7 +45,6 @@ func CreatePlaylist(cmd *m.CreatePlaylistCommand) error {
 }
 }
 
 
 func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
 func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
-	var err error
 	playlist := m.Playlist{
 	playlist := m.Playlist{
 		Id:       cmd.Id,
 		Id:       cmd.Id,
 		OrgId:    cmd.OrgId,
 		OrgId:    cmd.OrgId,
@@ -68,7 +65,7 @@ func UpdatePlaylist(cmd *m.UpdatePlaylistCommand) error {
 		Interval: playlist.Interval,
 		Interval: playlist.Interval,
 	}
 	}
 
 
-	_, err = x.Id(cmd.Id).Cols("id", "name", "interval").Update(&playlist)
+	_, err := x.ID(cmd.Id).Cols("id", "name", "interval").Update(&playlist)
 
 
 	if err != nil {
 	if err != nil {
 		return err
 		return err
@@ -104,7 +101,7 @@ func GetPlaylist(query *m.GetPlaylistByIdQuery) error {
 	}
 	}
 
 
 	playlist := m.Playlist{}
 	playlist := m.Playlist{}
-	_, err := x.Id(query.Id).Get(&playlist)
+	_, err := x.ID(query.Id).Get(&playlist)
 
 
 	query.Result = &playlist
 	query.Result = &playlist
 
 

+ 64 - 61
pkg/services/sqlstore/stats.go

@@ -18,7 +18,7 @@ var activeUserTimeLimit time.Duration = time.Hour * 24 * 30
 func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
 func GetDataSourceStats(query *m.GetDataSourceStatsQuery) error {
 	var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
 	var rawSql = `SELECT COUNT(*) as count, type FROM data_source GROUP BY type`
 	query.Result = make([]*m.DataSourceStats, 0)
 	query.Result = make([]*m.DataSourceStats, 0)
-	err := x.Sql(rawSql).Find(&query.Result)
+	err := x.SQL(rawSql).Find(&query.Result)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -30,36 +30,39 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
 	var rawSql = `SELECT
 	var rawSql = `SELECT
 			(
 			(
 				SELECT COUNT(*)
 				SELECT COUNT(*)
-        FROM ` + dialect.Quote("user") + `
-      ) AS users,
+		FROM ` + dialect.Quote("user") + `
+	  ) AS users,
 			(
 			(
 				SELECT COUNT(*)
 				SELECT COUNT(*)
-        FROM ` + dialect.Quote("org") + `
-      ) AS orgs,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("dashboard") + `
-      ) AS dashboards,
-			(
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("data_source") + `
-      ) AS datasources,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("playlist") + `
-      ) AS playlists,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("alert") + `
-      ) AS alerts,
+		FROM ` + dialect.Quote("org") + `
+	  ) AS orgs,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("dashboard") + `
+	  ) AS dashboards,
+		(
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("data_source") + `
+	  ) AS datasources,
+	  (
+		SELECT COUNT(*) FROM ` + dialect.Quote("star") + `
+	  ) AS stars,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("playlist") + `
+	  ) AS playlists,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("alert") + `
+	  ) AS alerts,
 			(
 			(
 				SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` where last_seen_at > ?
 				SELECT COUNT(*) FROM ` + dialect.Quote("user") + ` where last_seen_at > ?
-      ) as active_users
+	  ) as active_users
 			`
 			`
 
 
 	activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
 	activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
 	var stats m.SystemStats
 	var stats m.SystemStats
-	_, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
+	_, err := x.SQL(rawSql, activeUserDeadlineDate).Get(&stats)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}
@@ -70,51 +73,51 @@ func GetSystemStats(query *m.GetSystemStatsQuery) error {
 
 
 func GetAdminStats(query *m.GetAdminStatsQuery) error {
 func GetAdminStats(query *m.GetAdminStatsQuery) error {
 	var rawSql = `SELECT
 	var rawSql = `SELECT
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("user") + `
-      ) AS users,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("org") + `
-      ) AS orgs,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("dashboard") + `
-      ) AS dashboards,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("dashboard_snapshot") + `
-      ) AS snapshots,
-      (
-        SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
-        FROM ` + dialect.Quote("dashboard_tag") + `
-      ) AS tags,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("data_source") + `
-      ) AS datasources,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("playlist") + `
-      ) AS playlists,
-      (
-        SELECT COUNT(*) FROM ` + dialect.Quote("star") + `
-      ) AS stars,
-      (
-        SELECT COUNT(*)
-        FROM ` + dialect.Quote("alert") + `
-      ) AS alerts,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("user") + `
+	  ) AS users,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("org") + `
+	  ) AS orgs,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("dashboard") + `
+	  ) AS dashboards,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("dashboard_snapshot") + `
+	  ) AS snapshots,
+	  (
+		SELECT COUNT( DISTINCT ( ` + dialect.Quote("term") + ` ))
+		FROM ` + dialect.Quote("dashboard_tag") + `
+	  ) AS tags,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("data_source") + `
+	  ) AS datasources,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("playlist") + `
+	  ) AS playlists,
+	  (
+		SELECT COUNT(*) FROM ` + dialect.Quote("star") + `
+	  ) AS stars,
+	  (
+		SELECT COUNT(*)
+		FROM ` + dialect.Quote("alert") + `
+	  ) AS alerts,
 			(
 			(
 				SELECT COUNT(*)
 				SELECT COUNT(*)
-        from ` + dialect.Quote("user") + ` where last_seen_at > ?
+		from ` + dialect.Quote("user") + ` where last_seen_at > ?
 			) as active_users
 			) as active_users
-      `
+	  `
 
 
 	activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
 	activeUserDeadlineDate := time.Now().Add(-activeUserTimeLimit)
 
 
 	var stats m.AdminStats
 	var stats m.AdminStats
-	_, err := x.Sql(rawSql, activeUserDeadlineDate).Get(&stats)
+	_, err := x.SQL(rawSql, activeUserDeadlineDate).Get(&stats)
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 116 - 43
pkg/social/generic_oauth.go

@@ -1,18 +1,21 @@
 package social
 package social
 
 
 import (
 import (
+	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"errors"
 	"errors"
 	"fmt"
 	"fmt"
 	"net/http"
 	"net/http"
+	"net/mail"
+	"regexp"
 
 
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/models"
 
 
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2"
 )
 )
 
 
-type GenericOAuth struct {
-	*oauth2.Config
+type SocialGenericOAuth struct {
+	*SocialBase
 	allowedDomains       []string
 	allowedDomains       []string
 	allowedOrganizations []string
 	allowedOrganizations []string
 	apiUrl               string
 	apiUrl               string
@@ -20,19 +23,19 @@ type GenericOAuth struct {
 	teamIds              []int
 	teamIds              []int
 }
 }
 
 
-func (s *GenericOAuth) Type() int {
+func (s *SocialGenericOAuth) Type() int {
 	return int(models.GENERIC)
 	return int(models.GENERIC)
 }
 }
 
 
-func (s *GenericOAuth) IsEmailAllowed(email string) bool {
+func (s *SocialGenericOAuth) IsEmailAllowed(email string) bool {
 	return isEmailAllowed(email, s.allowedDomains)
 	return isEmailAllowed(email, s.allowedDomains)
 }
 }
 
 
-func (s *GenericOAuth) IsSignupAllowed() bool {
+func (s *SocialGenericOAuth) IsSignupAllowed() bool {
 	return s.allowSignup
 	return s.allowSignup
 }
 }
 
 
-func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
+func (s *SocialGenericOAuth) IsTeamMember(client *http.Client) bool {
 	if len(s.teamIds) == 0 {
 	if len(s.teamIds) == 0 {
 		return true
 		return true
 	}
 	}
@@ -53,7 +56,7 @@ func (s *GenericOAuth) IsTeamMember(client *http.Client) bool {
 	return false
 	return false
 }
 }
 
 
-func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
+func (s *SocialGenericOAuth) IsOrganizationMember(client *http.Client) bool {
 	if len(s.allowedOrganizations) == 0 {
 	if len(s.allowedOrganizations) == 0 {
 		return true
 		return true
 	}
 	}
@@ -74,7 +77,7 @@ func (s *GenericOAuth) IsOrganizationMember(client *http.Client) bool {
 	return false
 	return false
 }
 }
 
 
-func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
+func (s *SocialGenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 	type Record struct {
 	type Record struct {
 		Email       string `json:"email"`
 		Email       string `json:"email"`
 		Primary     bool   `json:"primary"`
 		Primary     bool   `json:"primary"`
@@ -115,7 +118,7 @@ func (s *GenericOAuth) FetchPrivateEmail(client *http.Client) (string, error) {
 	return email, nil
 	return email, nil
 }
 }
 
 
-func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
+func (s *SocialGenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error) {
 	type Record struct {
 	type Record struct {
 		Id int `json:"id"`
 		Id int `json:"id"`
 	}
 	}
@@ -140,7 +143,7 @@ func (s *GenericOAuth) FetchTeamMemberships(client *http.Client) ([]int, error)
 	return ids, nil
 	return ids, nil
 }
 }
 
 
-func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
+func (s *SocialGenericOAuth) FetchOrganizations(client *http.Client) ([]string, error) {
 	type Record struct {
 	type Record struct {
 		Login string `json:"login"`
 		Login string `json:"login"`
 	}
 	}
@@ -165,62 +168,132 @@ func (s *GenericOAuth) FetchOrganizations(client *http.Client) ([]string, error)
 	return logins, nil
 	return logins, nil
 }
 }
 
 
-func (s *GenericOAuth) UserInfo(client *http.Client) (*BasicUserInfo, error) {
-	var data struct {
-		Name        string              `json:"name"`
-		DisplayName string              `json:"display_name"`
-		Login       string              `json:"login"`
-		Username    string              `json:"username"`
-		Email       string              `json:"email"`
-		Attributes  map[string][]string `json:"attributes"`
+type UserInfoJson struct {
+	Name        string              `json:"name"`
+	DisplayName string              `json:"display_name"`
+	Login       string              `json:"login"`
+	Username    string              `json:"username"`
+	Email       string              `json:"email"`
+	Upn         string              `json:"upn"`
+	Attributes  map[string][]string `json:"attributes"`
+}
+
+func (s *SocialGenericOAuth) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
+	var data UserInfoJson
+
+	if s.extractToken(&data, token) != true {
+		response, err := HttpGet(client, s.apiUrl)
+		if err != nil {
+			return nil, fmt.Errorf("Error getting user info: %s", err)
+		}
+
+		err = json.Unmarshal(response.Body, &data)
+		if err != nil {
+			return nil, fmt.Errorf("Error decoding user info JSON: %s", err)
+		}
+	}
+
+	name, err := s.extractName(data)
+	if err != nil {
+		return nil, err
 	}
 	}
 
 
-	response, err := HttpGet(client, s.apiUrl)
+	email, err := s.extractEmail(data, client)
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("Error getting user info: %s", err)
+		return nil, err
 	}
 	}
 
 
-	err = json.Unmarshal(response.Body, &data)
+	login, err := s.extractLogin(data, email)
 	if err != nil {
 	if err != nil {
-		return nil, fmt.Errorf("Error getting user info: %s", err)
+		return nil, err
 	}
 	}
 
 
 	userInfo := &BasicUserInfo{
 	userInfo := &BasicUserInfo{
-		Name:  data.Name,
-		Login: data.Login,
-		Email: data.Email,
+		Name:  name,
+		Login: login,
+		Email: email,
 	}
 	}
 
 
-	if userInfo.Email == "" && data.Attributes["email:primary"] != nil {
-		userInfo.Email = data.Attributes["email:primary"][0]
+	if !s.IsTeamMember(client) {
+		return nil, errors.New("User not a member of one of the required teams")
 	}
 	}
 
 
-	if userInfo.Email == "" {
-		userInfo.Email, err = s.FetchPrivateEmail(client)
-		if err != nil {
-			return nil, err
-		}
+	if !s.IsOrganizationMember(client) {
+		return nil, errors.New("User not a member of one of the required organizations")
 	}
 	}
 
 
-	if userInfo.Name == "" && data.DisplayName != "" {
-		userInfo.Name = data.DisplayName
+	return userInfo, nil
+}
+
+func (s *SocialGenericOAuth) extractToken(data *UserInfoJson, token *oauth2.Token) bool {
+	idToken := token.Extra("id_token")
+	if idToken == nil {
+		s.log.Debug("No id_token found", "token", token)
+		return false
 	}
 	}
 
 
-	if userInfo.Login == "" && data.Username != "" {
-		userInfo.Login = data.Username
+	jwtRegexp := regexp.MustCompile("^([-_a-zA-Z0-9]+)[.]([-_a-zA-Z0-9]+)[.]([-_a-zA-Z0-9]+)$")
+	matched := jwtRegexp.FindStringSubmatch(idToken.(string))
+	if matched == nil {
+		s.log.Debug("id_token is not in JWT format", "id_token", idToken.(string))
+		return false
 	}
 	}
 
 
-	if userInfo.Login == "" {
-		userInfo.Login = data.Email
+	payload, err := base64.RawURLEncoding.DecodeString(matched[2])
+	if err != nil {
+		s.log.Error("Error base64 decoding id_token", "raw_payload", matched[2], "err", err)
+		return false
 	}
 	}
 
 
-	if !s.IsTeamMember(client) {
-		return nil, errors.New("User not a member of one of the required teams")
+	err = json.Unmarshal(payload, data)
+	if err != nil {
+		s.log.Error("Error decoding id_token JSON", "payload", string(payload), "err", err)
+		return false
 	}
 	}
 
 
-	if !s.IsOrganizationMember(client) {
-		return nil, errors.New("User not a member of one of the required organizations")
+	s.log.Debug("Received id_token", "json", string(payload), "data", data)
+	return true
+}
+
+func (s *SocialGenericOAuth) extractEmail(data UserInfoJson, client *http.Client) (string, error) {
+	if data.Email != "" {
+		return data.Email, nil
 	}
 	}
 
 
-	return userInfo, nil
+	if data.Attributes["email:primary"] != nil {
+		return data.Attributes["email:primary"][0], nil
+	}
+
+	if data.Upn != "" {
+		emailAddr, emailErr := mail.ParseAddress(data.Upn)
+		if emailErr == nil {
+			return emailAddr.Address, nil
+		}
+	}
+
+	return s.FetchPrivateEmail(client)
+}
+
+func (s *SocialGenericOAuth) extractLogin(data UserInfoJson, email string) (string, error) {
+	if data.Login != "" {
+		return data.Login, nil
+	}
+
+	if data.Username != "" {
+		return data.Username, nil
+	}
+
+	return email, nil
+}
+
+func (s *SocialGenericOAuth) extractName(data UserInfoJson) (string, error) {
+	if data.Name != "" {
+		return data.Name, nil
+	}
+
+	if data.DisplayName != "" {
+		return data.DisplayName, nil
+	}
+
+	return "", nil
 }
 }

+ 2 - 2
pkg/social/github_oauth.go

@@ -12,7 +12,7 @@ import (
 )
 )
 
 
 type SocialGithub struct {
 type SocialGithub struct {
-	*oauth2.Config
+	*SocialBase
 	allowedDomains       []string
 	allowedDomains       []string
 	allowedOrganizations []string
 	allowedOrganizations []string
 	apiUrl               string
 	apiUrl               string
@@ -192,7 +192,7 @@ func (s *SocialGithub) FetchOrganizations(client *http.Client, organizationsUrl
 	return logins, nil
 	return logins, nil
 }
 }
 
 
-func (s *SocialGithub) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGithub) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 
 
 	var data struct {
 	var data struct {
 		Id               int    `json:"id"`
 		Id               int    `json:"id"`

+ 2 - 2
pkg/social/google_oauth.go

@@ -11,7 +11,7 @@ import (
 )
 )
 
 
 type SocialGoogle struct {
 type SocialGoogle struct {
-	*oauth2.Config
+	*SocialBase
 	allowedDomains []string
 	allowedDomains []string
 	hostedDomain   string
 	hostedDomain   string
 	apiUrl         string
 	apiUrl         string
@@ -30,7 +30,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool {
 	return s.allowSignup
 	return s.allowSignup
 }
 }
 
 
-func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 	var data struct {
 		Name  string `json:"name"`
 		Name  string `json:"name"`
 		Email string `json:"email"`
 		Email string `json:"email"`

+ 2 - 2
pkg/social/grafana_com_oauth.go

@@ -11,7 +11,7 @@ import (
 )
 )
 
 
 type SocialGrafanaCom struct {
 type SocialGrafanaCom struct {
-	*oauth2.Config
+	*SocialBase
 	url                  string
 	url                  string
 	allowedOrganizations []string
 	allowedOrganizations []string
 	allowSignup          bool
 	allowSignup          bool
@@ -49,7 +49,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool
 	return false
 	return false
 }
 }
 
 
-func (s *SocialGrafanaCom) UserInfo(client *http.Client) (*BasicUserInfo, error) {
+func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) {
 	var data struct {
 	var data struct {
 		Name  string      `json:"name"`
 		Name  string      `json:"name"`
 		Login string      `json:"username"`
 		Login string      `json:"username"`

+ 28 - 7
pkg/social/social.go

@@ -4,9 +4,11 @@ import (
 	"net/http"
 	"net/http"
 	"strings"
 	"strings"
 
 
-	"golang.org/x/net/context"
+	"context"
+
 	"golang.org/x/oauth2"
 	"golang.org/x/oauth2"
 
 
+	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util"
 	"github.com/grafana/grafana/pkg/util"
 )
 )
@@ -21,7 +23,7 @@ type BasicUserInfo struct {
 
 
 type SocialConnector interface {
 type SocialConnector interface {
 	Type() int
 	Type() int
-	UserInfo(client *http.Client) (*BasicUserInfo, error)
+	UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error)
 	IsEmailAllowed(email string) bool
 	IsEmailAllowed(email string) bool
 	IsSignupAllowed() bool
 	IsSignupAllowed() bool
 
 
@@ -30,6 +32,11 @@ type SocialConnector interface {
 	Client(ctx context.Context, t *oauth2.Token) *http.Client
 	Client(ctx context.Context, t *oauth2.Token) *http.Client
 }
 }
 
 
+type SocialBase struct {
+	*oauth2.Config
+	log log.Logger
+}
+
 type Error struct {
 type Error struct {
 	s string
 	s string
 }
 }
@@ -90,10 +97,15 @@ func NewOAuthService() {
 			Scopes:      info.Scopes,
 			Scopes:      info.Scopes,
 		}
 		}
 
 
+		logger := log.New("oauth." + name)
+
 		// GitHub.
 		// GitHub.
 		if name == "github" {
 		if name == "github" {
 			SocialMap["github"] = &SocialGithub{
 			SocialMap["github"] = &SocialGithub{
-				Config:               &config,
+				SocialBase: &SocialBase{
+					Config: &config,
+					log:    logger,
+				},
 				allowedDomains:       info.AllowedDomains,
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
 				allowSignup:          info.AllowSignup,
@@ -105,7 +117,10 @@ func NewOAuthService() {
 		// Google.
 		// Google.
 		if name == "google" {
 		if name == "google" {
 			SocialMap["google"] = &SocialGoogle{
 			SocialMap["google"] = &SocialGoogle{
-				Config:         &config,
+				SocialBase: &SocialBase{
+					Config: &config,
+					log:    logger,
+				},
 				allowedDomains: info.AllowedDomains,
 				allowedDomains: info.AllowedDomains,
 				hostedDomain:   info.HostedDomain,
 				hostedDomain:   info.HostedDomain,
 				apiUrl:         info.ApiUrl,
 				apiUrl:         info.ApiUrl,
@@ -115,8 +130,11 @@ func NewOAuthService() {
 
 
 		// Generic - Uses the same scheme as Github.
 		// Generic - Uses the same scheme as Github.
 		if name == "generic_oauth" {
 		if name == "generic_oauth" {
-			SocialMap["generic_oauth"] = &GenericOAuth{
-				Config:               &config,
+			SocialMap["generic_oauth"] = &SocialGenericOAuth{
+				SocialBase: &SocialBase{
+					Config: &config,
+					log:    logger,
+				},
 				allowedDomains:       info.AllowedDomains,
 				allowedDomains:       info.AllowedDomains,
 				apiUrl:               info.ApiUrl,
 				apiUrl:               info.ApiUrl,
 				allowSignup:          info.AllowSignup,
 				allowSignup:          info.AllowSignup,
@@ -138,7 +156,10 @@ func NewOAuthService() {
 			}
 			}
 
 
 			SocialMap["grafana_com"] = &SocialGrafanaCom{
 			SocialMap["grafana_com"] = &SocialGrafanaCom{
-				Config:               &config,
+				SocialBase: &SocialBase{
+					Config: &config,
+					log:    logger,
+				},
 				url:                  setting.GrafanaComUrl,
 				url:                  setting.GrafanaComUrl,
 				allowSignup:          info.AllowSignup,
 				allowSignup:          info.AllowSignup,
 				allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),
 				allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()),

+ 50 - 16
pkg/tsdb/cloudwatch/metric_find_query.go

@@ -3,6 +3,7 @@ package cloudwatch
 import (
 import (
 	"context"
 	"context"
 	"errors"
 	"errors"
+	"fmt"
 	"reflect"
 	"reflect"
 	"sort"
 	"sort"
 	"strings"
 	"strings"
@@ -187,18 +188,6 @@ func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryCo
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
 		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
 		break
 		break
 	case "ec2_instance_attribute":
 	case "ec2_instance_attribute":
-		region := parameters.Get("region").MustString()
-		dsInfo := e.getDsInfo(region)
-		cfg, err := e.getAwsConfig(dsInfo)
-		if err != nil {
-			return nil, errors.New("Failed to call ec2:DescribeInstances")
-		}
-		sess, err := session.NewSession(cfg)
-		if err != nil {
-			return nil, errors.New("Failed to call ec2:DescribeInstances")
-		}
-		e.ec2Svc = ec2.New(sess, cfg)
-
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
 		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
 		break
 		break
 	}
 	}
@@ -226,6 +215,21 @@ func transformToTable(data []suggestData, result *tsdb.QueryResult) {
 	result.Meta.Set("rowCount", len(data))
 	result.Meta.Set("rowCount", len(data))
 }
 }
 
 
+func parseMultiSelectValue(input string) []string {
+	trimmedInput := strings.TrimSpace(input)
+
+	if strings.HasPrefix(trimmedInput, "{") {
+		values := strings.Split(strings.TrimRight(strings.TrimLeft(trimmedInput, "{"), "}"), ",")
+		trimValues := make([]string, len(values))
+		for i, v := range values {
+			trimValues[i] = strings.TrimSpace(v)
+		}
+		return trimValues
+	} else {
+		return []string{trimmedInput}
+	}
+}
+
 // Whenever this list is updated, frontend list should also be updated.
 // Whenever this list is updated, frontend list should also be updated.
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
@@ -364,19 +368,44 @@ func (e *CloudWatchExecutor) handleGetDimensionValues(ctx context.Context, param
 	return result, nil
 	return result, nil
 }
 }
 
 
+func (e *CloudWatchExecutor) ensureClientSession(region string) error {
+	if e.ec2Svc == nil {
+		dsInfo := e.getDsInfo(region)
+		cfg, err := e.getAwsConfig(dsInfo)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:getAwsConfig, %v", err)
+		}
+		sess, err := session.NewSession(cfg)
+		if err != nil {
+			return fmt.Errorf("Failed to call ec2:NewSession, %v", err)
+		}
+		e.ec2Svc = ec2.New(sess, cfg)
+	}
+	return nil
+}
+
 func (e *CloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 func (e *CloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 	region := parameters.Get("region").MustString()
 	region := parameters.Get("region").MustString()
 	instanceId := parameters.Get("instanceId").MustString()
 	instanceId := parameters.Get("instanceId").MustString()
 
 
-	instanceIds := []*string{aws.String(instanceId)}
+	err := e.ensureClientSession(region)
+	if err != nil {
+		return nil, err
+	}
+
+	instanceIds := aws.StringSlice(parseMultiSelectValue(instanceId))
 	instances, err := e.ec2DescribeInstances(region, nil, instanceIds)
 	instances, err := e.ec2DescribeInstances(region, nil, instanceIds)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
 
 
 	result := make([]suggestData, 0)
 	result := make([]suggestData, 0)
-	for _, mapping := range instances.Reservations[0].Instances[0].BlockDeviceMappings {
-		result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId})
+	for _, reservation := range instances.Reservations {
+		for _, instance := range reservation.Instances {
+			for _, mapping := range instance.BlockDeviceMappings {
+				result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId})
+			}
+		}
 	}
 	}
 
 
 	return result, nil
 	return result, nil
@@ -403,6 +432,11 @@ func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context,
 		}
 		}
 	}
 	}
 
 
+	err := e.ensureClientSession(region)
+	if err != nil {
+		return nil, err
+	}
+
 	instances, err := e.ec2DescribeInstances(region, filters, nil)
 	instances, err := e.ec2DescribeInstances(region, filters, nil)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -478,7 +512,7 @@ func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace stri
 			return !lastPage
 			return !lastPage
 		})
 		})
 	if err != nil {
 	if err != nil {
-		return nil, errors.New("Failed to call cloudwatch:ListMetrics")
+		return nil, fmt.Errorf("Failed to call cloudwatch:ListMetrics, %v", err)
 	}
 	}
 
 
 	return &resp, nil
 	return &resp, nil

+ 82 - 0
pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -8,6 +8,7 @@ import (
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/ec2"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
 	"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
+	"github.com/bmizerany/assert"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/tsdb"
 	"github.com/grafana/grafana/pkg/tsdb"
 	. "github.com/smartystreets/goconvey/convey"
 	. "github.com/smartystreets/goconvey/convey"
@@ -114,4 +115,85 @@ func TestCloudWatchMetrics(t *testing.T) {
 			So(result[0].Text, ShouldEqual, "i-12345678")
 			So(result[0].Text, ShouldEqual, "i-12345678")
 		})
 		})
 	})
 	})
+
+	Convey("When calling handleGetEbsVolumeIds", t, func() {
+
+		executor := &CloudWatchExecutor{
+			ec2Svc: mockedEc2{Resp: ec2.DescribeInstancesOutput{
+				Reservations: []*ec2.Reservation{
+					{
+						Instances: []*ec2.Instance{
+							{
+								InstanceId: aws.String("i-1"),
+								BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-1")}},
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-1-2")}},
+								},
+							},
+							{
+								InstanceId: aws.String("i-2"),
+								BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-1")}},
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-2-2")}},
+								},
+							},
+						},
+					},
+					{
+						Instances: []*ec2.Instance{
+							{
+								InstanceId: aws.String("i-3"),
+								BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-1")}},
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-3-2")}},
+								},
+							},
+							{
+								InstanceId: aws.String("i-4"),
+								BlockDeviceMappings: []*ec2.InstanceBlockDeviceMapping{
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-1")}},
+									{Ebs: &ec2.EbsInstanceBlockDevice{VolumeId: aws.String("vol-4-2")}},
+								},
+							},
+						},
+					},
+				},
+			}},
+		}
+
+		json := simplejson.New()
+		json.Set("region", "us-east-1")
+		json.Set("instanceId", "{i-1, i-2, i-3, i-4}")
+		result, _ := executor.handleGetEbsVolumeIds(context.Background(), json, &tsdb.TsdbQuery{})
+
+		Convey("Should return all 8 VolumeIds", func() {
+			So(len(result), ShouldEqual, 8)
+			So(result[0].Text, ShouldEqual, "vol-1-1")
+			So(result[1].Text, ShouldEqual, "vol-1-2")
+			So(result[2].Text, ShouldEqual, "vol-2-1")
+			So(result[3].Text, ShouldEqual, "vol-2-2")
+			So(result[4].Text, ShouldEqual, "vol-3-1")
+			So(result[5].Text, ShouldEqual, "vol-3-2")
+			So(result[6].Text, ShouldEqual, "vol-4-1")
+			So(result[7].Text, ShouldEqual, "vol-4-2")
+		})
+	})
+}
+
+func TestParseMultiSelectValue(t *testing.T) {
+
+	var values []string
+
+	values = parseMultiSelectValue(" i-someInstance ")
+	assert.Equal(t, []string{"i-someInstance"}, values)
+
+	values = parseMultiSelectValue("{i-05}")
+	assert.Equal(t, []string{"i-05"}, values)
+
+	values = parseMultiSelectValue(" {i-01, i-03, i-04} ")
+	assert.Equal(t, []string{"i-01", "i-03", "i-04"}, values)
+
+	values = parseMultiSelectValue("i-{01}")
+	assert.Equal(t, []string{"i-{01}"}, values)
+
 }
 }

+ 0 - 636
pkg/tsdb/models/tsdb_plugin.pb.go

@@ -1,636 +0,0 @@
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// source: tsdb_plugin.proto
-
-/*
-Package proto is a generated protocol buffer package.
-
-It is generated from these files:
-	tsdb_plugin.proto
-
-It has these top-level messages:
-	TsdbQuery
-	Query
-	TimeRange
-	Response
-	QueryResult
-	Table
-	TableColumn
-	TableRow
-	RowValue
-	DatasourceInfo
-	TimeSeries
-	Point
-*/
-package proto
-
-import proto1 "github.com/golang/protobuf/proto"
-import fmt "fmt"
-import math "math"
-
-import (
-	context "golang.org/x/net/context"
-	grpc "google.golang.org/grpc"
-)
-
-// Reference imports to suppress errors if they are not otherwise used.
-var _ = proto1.Marshal
-var _ = fmt.Errorf
-var _ = math.Inf
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the proto package it is being compiled against.
-// A compilation error at this line likely means your copy of the
-// proto package needs to be updated.
-const _ = proto1.ProtoPackageIsVersion2 // please upgrade the proto package
-
-type RowValue_Kind int32
-
-const (
-	// Field type null.
-	RowValue_TYPE_NULL RowValue_Kind = 0
-	// Field type double.
-	RowValue_TYPE_DOUBLE RowValue_Kind = 1
-	// Field type int64.
-	RowValue_TYPE_INT64 RowValue_Kind = 2
-	// Field type bool.
-	RowValue_TYPE_BOOL RowValue_Kind = 3
-	// Field type string.
-	RowValue_TYPE_STRING RowValue_Kind = 4
-	// Field type bytes.
-	RowValue_TYPE_BYTES RowValue_Kind = 5
-)
-
-var RowValue_Kind_name = map[int32]string{
-	0: "TYPE_NULL",
-	1: "TYPE_DOUBLE",
-	2: "TYPE_INT64",
-	3: "TYPE_BOOL",
-	4: "TYPE_STRING",
-	5: "TYPE_BYTES",
-}
-var RowValue_Kind_value = map[string]int32{
-	"TYPE_NULL":   0,
-	"TYPE_DOUBLE": 1,
-	"TYPE_INT64":  2,
-	"TYPE_BOOL":   3,
-	"TYPE_STRING": 4,
-	"TYPE_BYTES":  5,
-}
-
-func (x RowValue_Kind) String() string {
-	return proto1.EnumName(RowValue_Kind_name, int32(x))
-}
-func (RowValue_Kind) EnumDescriptor() ([]byte, []int) { return fileDescriptor0, []int{8, 0} }
-
-type TsdbQuery struct {
-	TimeRange  *TimeRange      `protobuf:"bytes,1,opt,name=timeRange" json:"timeRange,omitempty"`
-	Datasource *DatasourceInfo `protobuf:"bytes,2,opt,name=datasource" json:"datasource,omitempty"`
-	Queries    []*Query        `protobuf:"bytes,3,rep,name=queries" json:"queries,omitempty"`
-}
-
-func (m *TsdbQuery) Reset()                    { *m = TsdbQuery{} }
-func (m *TsdbQuery) String() string            { return proto1.CompactTextString(m) }
-func (*TsdbQuery) ProtoMessage()               {}
-func (*TsdbQuery) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} }
-
-func (m *TsdbQuery) GetTimeRange() *TimeRange {
-	if m != nil {
-		return m.TimeRange
-	}
-	return nil
-}
-
-func (m *TsdbQuery) GetDatasource() *DatasourceInfo {
-	if m != nil {
-		return m.Datasource
-	}
-	return nil
-}
-
-func (m *TsdbQuery) GetQueries() []*Query {
-	if m != nil {
-		return m.Queries
-	}
-	return nil
-}
-
-type Query struct {
-	RefId         string `protobuf:"bytes,1,opt,name=refId" json:"refId,omitempty"`
-	MaxDataPoints int64  `protobuf:"varint,2,opt,name=maxDataPoints" json:"maxDataPoints,omitempty"`
-	IntervalMs    int64  `protobuf:"varint,3,opt,name=intervalMs" json:"intervalMs,omitempty"`
-	ModelJson     string `protobuf:"bytes,4,opt,name=modelJson" json:"modelJson,omitempty"`
-}
-
-func (m *Query) Reset()                    { *m = Query{} }
-func (m *Query) String() string            { return proto1.CompactTextString(m) }
-func (*Query) ProtoMessage()               {}
-func (*Query) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} }
-
-func (m *Query) GetRefId() string {
-	if m != nil {
-		return m.RefId
-	}
-	return ""
-}
-
-func (m *Query) GetMaxDataPoints() int64 {
-	if m != nil {
-		return m.MaxDataPoints
-	}
-	return 0
-}
-
-func (m *Query) GetIntervalMs() int64 {
-	if m != nil {
-		return m.IntervalMs
-	}
-	return 0
-}
-
-func (m *Query) GetModelJson() string {
-	if m != nil {
-		return m.ModelJson
-	}
-	return ""
-}
-
-type TimeRange struct {
-	FromRaw     string `protobuf:"bytes,1,opt,name=fromRaw" json:"fromRaw,omitempty"`
-	ToRaw       string `protobuf:"bytes,2,opt,name=toRaw" json:"toRaw,omitempty"`
-	FromEpochMs int64  `protobuf:"varint,3,opt,name=fromEpochMs" json:"fromEpochMs,omitempty"`
-	ToEpochMs   int64  `protobuf:"varint,4,opt,name=toEpochMs" json:"toEpochMs,omitempty"`
-}
-
-func (m *TimeRange) Reset()                    { *m = TimeRange{} }
-func (m *TimeRange) String() string            { return proto1.CompactTextString(m) }
-func (*TimeRange) ProtoMessage()               {}
-func (*TimeRange) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} }
-
-func (m *TimeRange) GetFromRaw() string {
-	if m != nil {
-		return m.FromRaw
-	}
-	return ""
-}
-
-func (m *TimeRange) GetToRaw() string {
-	if m != nil {
-		return m.ToRaw
-	}
-	return ""
-}
-
-func (m *TimeRange) GetFromEpochMs() int64 {
-	if m != nil {
-		return m.FromEpochMs
-	}
-	return 0
-}
-
-func (m *TimeRange) GetToEpochMs() int64 {
-	if m != nil {
-		return m.ToEpochMs
-	}
-	return 0
-}
-
-type Response struct {
-	Results []*QueryResult `protobuf:"bytes,1,rep,name=results" json:"results,omitempty"`
-}
-
-func (m *Response) Reset()                    { *m = Response{} }
-func (m *Response) String() string            { return proto1.CompactTextString(m) }
-func (*Response) ProtoMessage()               {}
-func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} }
-
-func (m *Response) GetResults() []*QueryResult {
-	if m != nil {
-		return m.Results
-	}
-	return nil
-}
-
-type QueryResult struct {
-	Error    string        `protobuf:"bytes,1,opt,name=error" json:"error,omitempty"`
-	RefId    string        `protobuf:"bytes,2,opt,name=refId" json:"refId,omitempty"`
-	MetaJson string        `protobuf:"bytes,3,opt,name=metaJson" json:"metaJson,omitempty"`
-	Series   []*TimeSeries `protobuf:"bytes,4,rep,name=series" json:"series,omitempty"`
-	Tables   []*Table      `protobuf:"bytes,5,rep,name=tables" json:"tables,omitempty"`
-}
-
-func (m *QueryResult) Reset()                    { *m = QueryResult{} }
-func (m *QueryResult) String() string            { return proto1.CompactTextString(m) }
-func (*QueryResult) ProtoMessage()               {}
-func (*QueryResult) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} }
-
-func (m *QueryResult) GetError() string {
-	if m != nil {
-		return m.Error
-	}
-	return ""
-}
-
-func (m *QueryResult) GetRefId() string {
-	if m != nil {
-		return m.RefId
-	}
-	return ""
-}
-
-func (m *QueryResult) GetMetaJson() string {
-	if m != nil {
-		return m.MetaJson
-	}
-	return ""
-}
-
-func (m *QueryResult) GetSeries() []*TimeSeries {
-	if m != nil {
-		return m.Series
-	}
-	return nil
-}
-
-func (m *QueryResult) GetTables() []*Table {
-	if m != nil {
-		return m.Tables
-	}
-	return nil
-}
-
-type Table struct {
-	Columns []*TableColumn `protobuf:"bytes,1,rep,name=columns" json:"columns,omitempty"`
-	Rows    []*TableRow    `protobuf:"bytes,2,rep,name=rows" json:"rows,omitempty"`
-}
-
-func (m *Table) Reset()                    { *m = Table{} }
-func (m *Table) String() string            { return proto1.CompactTextString(m) }
-func (*Table) ProtoMessage()               {}
-func (*Table) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} }
-
-func (m *Table) GetColumns() []*TableColumn {
-	if m != nil {
-		return m.Columns
-	}
-	return nil
-}
-
-func (m *Table) GetRows() []*TableRow {
-	if m != nil {
-		return m.Rows
-	}
-	return nil
-}
-
-type TableColumn struct {
-	Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
-}
-
-func (m *TableColumn) Reset()                    { *m = TableColumn{} }
-func (m *TableColumn) String() string            { return proto1.CompactTextString(m) }
-func (*TableColumn) ProtoMessage()               {}
-func (*TableColumn) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} }
-
-func (m *TableColumn) GetName() string {
-	if m != nil {
-		return m.Name
-	}
-	return ""
-}
-
-type TableRow struct {
-	Values []*RowValue `protobuf:"bytes,1,rep,name=values" json:"values,omitempty"`
-}
-
-func (m *TableRow) Reset()                    { *m = TableRow{} }
-func (m *TableRow) String() string            { return proto1.CompactTextString(m) }
-func (*TableRow) ProtoMessage()               {}
-func (*TableRow) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} }
-
-func (m *TableRow) GetValues() []*RowValue {
-	if m != nil {
-		return m.Values
-	}
-	return nil
-}
-
-type RowValue struct {
-	Kind        RowValue_Kind `protobuf:"varint,1,opt,name=kind,enum=plugins.RowValue_Kind" json:"kind,omitempty"`
-	DoubleValue float64       `protobuf:"fixed64,2,opt,name=doubleValue" json:"doubleValue,omitempty"`
-	Int64Value  int64         `protobuf:"varint,3,opt,name=int64Value" json:"int64Value,omitempty"`
-	BoolValue   bool          `protobuf:"varint,4,opt,name=boolValue" json:"boolValue,omitempty"`
-	StringValue string        `protobuf:"bytes,5,opt,name=stringValue" json:"stringValue,omitempty"`
-	BytesValue  []byte        `protobuf:"bytes,6,opt,name=bytesValue,proto3" json:"bytesValue,omitempty"`
-}
-
-func (m *RowValue) Reset()                    { *m = RowValue{} }
-func (m *RowValue) String() string            { return proto1.CompactTextString(m) }
-func (*RowValue) ProtoMessage()               {}
-func (*RowValue) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} }
-
-func (m *RowValue) GetKind() RowValue_Kind {
-	if m != nil {
-		return m.Kind
-	}
-	return RowValue_TYPE_NULL
-}
-
-func (m *RowValue) GetDoubleValue() float64 {
-	if m != nil {
-		return m.DoubleValue
-	}
-	return 0
-}
-
-func (m *RowValue) GetInt64Value() int64 {
-	if m != nil {
-		return m.Int64Value
-	}
-	return 0
-}
-
-func (m *RowValue) GetBoolValue() bool {
-	if m != nil {
-		return m.BoolValue
-	}
-	return false
-}
-
-func (m *RowValue) GetStringValue() string {
-	if m != nil {
-		return m.StringValue
-	}
-	return ""
-}
-
-func (m *RowValue) GetBytesValue() []byte {
-	if m != nil {
-		return m.BytesValue
-	}
-	return nil
-}
-
-type DatasourceInfo struct {
-	Id             int64  `protobuf:"varint,1,opt,name=id" json:"id,omitempty"`
-	OrgId          int64  `protobuf:"varint,2,opt,name=orgId" json:"orgId,omitempty"`
-	Name           string `protobuf:"bytes,3,opt,name=name" json:"name,omitempty"`
-	Type           string `protobuf:"bytes,4,opt,name=type" json:"type,omitempty"`
-	Url            string `protobuf:"bytes,5,opt,name=url" json:"url,omitempty"`
-	JsonData       string `protobuf:"bytes,6,opt,name=jsonData" json:"jsonData,omitempty"`
-	SecureJsonData string `protobuf:"bytes,7,opt,name=secureJsonData" json:"secureJsonData,omitempty"`
-}
-
-func (m *DatasourceInfo) Reset()                    { *m = DatasourceInfo{} }
-func (m *DatasourceInfo) String() string            { return proto1.CompactTextString(m) }
-func (*DatasourceInfo) ProtoMessage()               {}
-func (*DatasourceInfo) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} }
-
-func (m *DatasourceInfo) GetId() int64 {
-	if m != nil {
-		return m.Id
-	}
-	return 0
-}
-
-func (m *DatasourceInfo) GetOrgId() int64 {
-	if m != nil {
-		return m.OrgId
-	}
-	return 0
-}
-
-func (m *DatasourceInfo) GetName() string {
-	if m != nil {
-		return m.Name
-	}
-	return ""
-}
-
-func (m *DatasourceInfo) GetType() string {
-	if m != nil {
-		return m.Type
-	}
-	return ""
-}
-
-func (m *DatasourceInfo) GetUrl() string {
-	if m != nil {
-		return m.Url
-	}
-	return ""
-}
-
-func (m *DatasourceInfo) GetJsonData() string {
-	if m != nil {
-		return m.JsonData
-	}
-	return ""
-}
-
-func (m *DatasourceInfo) GetSecureJsonData() string {
-	if m != nil {
-		return m.SecureJsonData
-	}
-	return ""
-}
-
-type TimeSeries struct {
-	Name   string            `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
-	Tags   map[string]string `protobuf:"bytes,2,rep,name=tags" json:"tags,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
-	Points []*Point          `protobuf:"bytes,3,rep,name=points" json:"points,omitempty"`
-}
-
-func (m *TimeSeries) Reset()                    { *m = TimeSeries{} }
-func (m *TimeSeries) String() string            { return proto1.CompactTextString(m) }
-func (*TimeSeries) ProtoMessage()               {}
-func (*TimeSeries) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} }
-
-func (m *TimeSeries) GetName() string {
-	if m != nil {
-		return m.Name
-	}
-	return ""
-}
-
-func (m *TimeSeries) GetTags() map[string]string {
-	if m != nil {
-		return m.Tags
-	}
-	return nil
-}
-
-func (m *TimeSeries) GetPoints() []*Point {
-	if m != nil {
-		return m.Points
-	}
-	return nil
-}
-
-type Point struct {
-	Timestamp int64   `protobuf:"varint,1,opt,name=timestamp" json:"timestamp,omitempty"`
-	Value     float64 `protobuf:"fixed64,2,opt,name=value" json:"value,omitempty"`
-}
-
-func (m *Point) Reset()                    { *m = Point{} }
-func (m *Point) String() string            { return proto1.CompactTextString(m) }
-func (*Point) ProtoMessage()               {}
-func (*Point) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} }
-
-func (m *Point) GetTimestamp() int64 {
-	if m != nil {
-		return m.Timestamp
-	}
-	return 0
-}
-
-func (m *Point) GetValue() float64 {
-	if m != nil {
-		return m.Value
-	}
-	return 0
-}
-
-func init() {
-	proto1.RegisterType((*TsdbQuery)(nil), "plugins.TsdbQuery")
-	proto1.RegisterType((*Query)(nil), "plugins.Query")
-	proto1.RegisterType((*TimeRange)(nil), "plugins.TimeRange")
-	proto1.RegisterType((*Response)(nil), "plugins.Response")
-	proto1.RegisterType((*QueryResult)(nil), "plugins.QueryResult")
-	proto1.RegisterType((*Table)(nil), "plugins.Table")
-	proto1.RegisterType((*TableColumn)(nil), "plugins.TableColumn")
-	proto1.RegisterType((*TableRow)(nil), "plugins.TableRow")
-	proto1.RegisterType((*RowValue)(nil), "plugins.RowValue")
-	proto1.RegisterType((*DatasourceInfo)(nil), "plugins.DatasourceInfo")
-	proto1.RegisterType((*TimeSeries)(nil), "plugins.TimeSeries")
-	proto1.RegisterType((*Point)(nil), "plugins.Point")
-	proto1.RegisterEnum("plugins.RowValue_Kind", RowValue_Kind_name, RowValue_Kind_value)
-}
-
-// Reference imports to suppress errors if they are not otherwise used.
-var _ context.Context
-var _ grpc.ClientConn
-
-// This is a compile-time assertion to ensure that this generated file
-// is compatible with the grpc package it is being compiled against.
-const _ = grpc.SupportPackageIsVersion4
-
-// Client API for TsdbPlugin service
-
-type TsdbPluginClient interface {
-	Query(ctx context.Context, in *TsdbQuery, opts ...grpc.CallOption) (*Response, error)
-}
-
-type tsdbPluginClient struct {
-	cc *grpc.ClientConn
-}
-
-func NewTsdbPluginClient(cc *grpc.ClientConn) TsdbPluginClient {
-	return &tsdbPluginClient{cc}
-}
-
-func (c *tsdbPluginClient) Query(ctx context.Context, in *TsdbQuery, opts ...grpc.CallOption) (*Response, error) {
-	out := new(Response)
-	err := grpc.Invoke(ctx, "/plugins.TsdbPlugin/Query", in, out, c.cc, opts...)
-	if err != nil {
-		return nil, err
-	}
-	return out, nil
-}
-
-// Server API for TsdbPlugin service
-
-type TsdbPluginServer interface {
-	Query(context.Context, *TsdbQuery) (*Response, error)
-}
-
-func RegisterTsdbPluginServer(s *grpc.Server, srv TsdbPluginServer) {
-	s.RegisterService(&_TsdbPlugin_serviceDesc, srv)
-}
-
-func _TsdbPlugin_Query_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
-	in := new(TsdbQuery)
-	if err := dec(in); err != nil {
-		return nil, err
-	}
-	if interceptor == nil {
-		return srv.(TsdbPluginServer).Query(ctx, in)
-	}
-	info := &grpc.UnaryServerInfo{
-		Server:     srv,
-		FullMethod: "/plugins.TsdbPlugin/Query",
-	}
-	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
-		return srv.(TsdbPluginServer).Query(ctx, req.(*TsdbQuery))
-	}
-	return interceptor(ctx, in, info, handler)
-}
-
-var _TsdbPlugin_serviceDesc = grpc.ServiceDesc{
-	ServiceName: "plugins.TsdbPlugin",
-	HandlerType: (*TsdbPluginServer)(nil),
-	Methods: []grpc.MethodDesc{
-		{
-			MethodName: "Query",
-			Handler:    _TsdbPlugin_Query_Handler,
-		},
-	},
-	Streams:  []grpc.StreamDesc{},
-	Metadata: "tsdb_plugin.proto",
-}
-
-func init() { proto1.RegisterFile("tsdb_plugin.proto", fileDescriptor0) }
-
-var fileDescriptor0 = []byte{
-	// 811 bytes of a gzipped FileDescriptorProto
-	0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x6c, 0x55, 0x5d, 0x8f, 0x1b, 0x35,
-	0x14, 0x65, 0xbe, 0x92, 0xcc, 0x0d, 0x0d, 0xa9, 0xa9, 0x20, 0x5a, 0x01, 0x5a, 0x46, 0x50, 0x05,
-	0x90, 0x22, 0x08, 0xa5, 0x45, 0x85, 0xa7, 0xd0, 0x08, 0xa5, 0x84, 0xdd, 0xc5, 0x3b, 0x45, 0x2a,
-	0x0f, 0x54, 0x93, 0x8c, 0x13, 0x86, 0xce, 0x8c, 0x83, 0xed, 0xd9, 0x10, 0xf1, 0xc4, 0x3f, 0xe1,
-	0x99, 0x67, 0x7e, 0x00, 0x3f, 0xad, 0xf2, 0x1d, 0xcf, 0x47, 0xd2, 0x7d, 0x9a, 0xb9, 0xe7, 0x1c,
-	0x5f, 0x5f, 0x5f, 0x1f, 0xdb, 0x70, 0x57, 0xc9, 0x78, 0xf5, 0x62, 0x97, 0x16, 0xdb, 0x24, 0x9f,
-	0xec, 0x04, 0x57, 0x9c, 0x74, 0xcb, 0x48, 0x06, 0xff, 0x58, 0xe0, 0x87, 0x32, 0x5e, 0xfd, 0x54,
-	0x30, 0x71, 0x20, 0x9f, 0x83, 0xaf, 0x92, 0x8c, 0xd1, 0x28, 0xdf, 0xb2, 0x91, 0x75, 0x6e, 0x8d,
-	0xfb, 0x53, 0x32, 0x31, 0xd2, 0x49, 0x58, 0x31, 0xb4, 0x11, 0x91, 0x47, 0x00, 0x71, 0xa4, 0x22,
-	0xc9, 0x0b, 0xb1, 0x66, 0x23, 0x1b, 0x87, 0xbc, 0x5b, 0x0f, 0x79, 0x52, 0x53, 0x8b, 0x7c, 0xc3,
-	0x69, 0x4b, 0x4a, 0xc6, 0xd0, 0xfd, 0xa3, 0x60, 0x22, 0x61, 0x72, 0xe4, 0x9c, 0x3b, 0xe3, 0xfe,
-	0x74, 0x50, 0x8f, 0xc2, 0x5a, 0x68, 0x45, 0x07, 0x7f, 0x5b, 0xe0, 0x95, 0xe5, 0xdd, 0x03, 0x4f,
-	0xb0, 0xcd, 0x22, 0xc6, 0xd2, 0x7c, 0x5a, 0x06, 0xe4, 0x23, 0xb8, 0x93, 0x45, 0x7f, 0xea, 0xa9,
-	0xae, 0x78, 0x92, 0x2b, 0x89, 0x55, 0x38, 0xf4, 0x18, 0x24, 0x1f, 0x00, 0x24, 0xb9, 0x62, 0xe2,
-	0x26, 0x4a, 0x7f, 0xd4, 0x53, 0x6a, 0x49, 0x0b, 0x21, 0xef, 0x81, 0x9f, 0xf1, 0x98, 0xa5, 0x4f,
-	0x25, 0xcf, 0x47, 0x2e, 0xe6, 0x6f, 0x80, 0xe0, 0x2f, 0xf0, 0xeb, 0xe5, 0x93, 0x11, 0x74, 0x37,
-	0x82, 0x67, 0x34, 0xda, 0x9b, 0x42, 0xaa, 0x50, 0x17, 0xa8, 0xb8, 0xc6, 0xed, 0xb2, 0x40, 0x0c,
-	0xc8, 0x39, 0xf4, 0xb5, 0x60, 0xbe, 0xe3, 0xeb, 0xdf, 0xea, 0xb9, 0xdb, 0x90, 0x9e, 0x5c, 0xf1,
-	0x8a, 0x77, 0x91, 0x6f, 0x80, 0xe0, 0x31, 0xf4, 0x28, 0x93, 0x3b, 0x9e, 0x4b, 0x46, 0x26, 0xd0,
-	0x15, 0x4c, 0x16, 0xa9, 0x92, 0x23, 0x0b, 0xdb, 0x76, 0xef, 0xa4, 0x6d, 0x48, 0xd2, 0x4a, 0x14,
-	0xfc, 0x6b, 0x41, 0xbf, 0x45, 0xe8, 0x0a, 0x99, 0x10, 0x5c, 0x54, 0x2d, 0xc4, 0xa0, 0x69, 0xac,
-	0xdd, 0x6e, 0xec, 0x19, 0xf4, 0x32, 0xa6, 0x22, 0xec, 0x88, 0x83, 0x44, 0x1d, 0x93, 0xcf, 0xa0,
-	0x23, 0xcb, 0xdd, 0x73, 0xb1, 0x8c, 0xb7, 0x8f, 0x6c, 0x72, 0x8d, 0x14, 0x35, 0x12, 0x72, 0x1f,
-	0x3a, 0x2a, 0x5a, 0xa5, 0x4c, 0x8e, 0xbc, 0x93, 0xad, 0x0e, 0x35, 0x4c, 0x0d, 0x1b, 0xfc, 0x0a,
-	0x1e, 0x02, 0x7a, 0x95, 0x6b, 0x9e, 0x16, 0x59, 0xfe, 0xfa, 0x2a, 0x51, 0xf0, 0x1d, 0x92, 0xb4,
-	0x12, 0x91, 0x8f, 0xc1, 0x15, 0x7c, 0xaf, 0x77, 0x5e, 0x8b, 0xef, 0x9e, 0xa4, 0xe7, 0x7b, 0x8a,
-	0x74, 0xf0, 0x21, 0xf4, 0x5b, 0xc3, 0x09, 0x01, 0x37, 0x8f, 0x32, 0x66, 0x5a, 0x81, 0xff, 0xc1,
-	0x57, 0xd0, 0xab, 0x06, 0x91, 0x4f, 0xa0, 0x73, 0x13, 0xa5, 0x05, 0xab, 0x8a, 0x68, 0xf2, 0x52,
-	0xbe, 0xff, 0x59, 0x33, 0xd4, 0x08, 0x82, 0xff, 0x6d, 0xe8, 0x55, 0x20, 0xf9, 0x14, 0xdc, 0x97,
-	0x49, 0x5e, 0xba, 0x74, 0x30, 0x7d, 0xe7, 0xb5, 0x51, 0x93, 0x1f, 0x92, 0x3c, 0xa6, 0xa8, 0xd1,
-	0xde, 0x88, 0x79, 0xb1, 0x4a, 0x19, 0x32, 0xd8, 0x7f, 0x8b, 0xb6, 0x21, 0x63, 0xdc, 0x87, 0x0f,
-	0x4a, 0x41, 0x63, 0x5c, 0x83, 0x68, 0xef, 0xac, 0x38, 0x4f, 0x4b, 0x5a, 0x7b, 0xa7, 0x47, 0x1b,
-	0x40, 0xe7, 0x97, 0x4a, 0x24, 0xf9, 0xb6, 0xe4, 0x3d, 0x5c, 0x6a, 0x1b, 0xd2, 0xf9, 0x57, 0x07,
-	0xc5, 0x64, 0x29, 0xe8, 0x9c, 0x5b, 0xe3, 0x37, 0x69, 0x0b, 0x09, 0x36, 0xe0, 0xea, 0x7a, 0xc9,
-	0x1d, 0xf0, 0xc3, 0xe7, 0x57, 0xf3, 0x17, 0x17, 0xcf, 0x96, 0xcb, 0xe1, 0x1b, 0xe4, 0x2d, 0xe8,
-	0x63, 0xf8, 0xe4, 0xf2, 0xd9, 0x6c, 0x39, 0x1f, 0x5a, 0x64, 0x00, 0x80, 0xc0, 0xe2, 0x22, 0x7c,
-	0xf8, 0x60, 0x68, 0xd7, 0xfa, 0xd9, 0xe5, 0xe5, 0x72, 0xe8, 0xd4, 0xfa, 0xeb, 0x90, 0x2e, 0x2e,
-	0xbe, 0x1f, 0xba, 0xb5, 0x7e, 0xf6, 0x3c, 0x9c, 0x5f, 0x0f, 0xbd, 0xe0, 0x3f, 0x0b, 0x06, 0xc7,
-	0xf7, 0x05, 0x19, 0x80, 0x9d, 0x94, 0x6d, 0x74, 0xa8, 0x9d, 0xc4, 0xda, 0xa6, 0x5c, 0x6c, 0x8d,
-	0x4d, 0x1d, 0x5a, 0x06, 0xf5, 0x36, 0x3a, 0xcd, 0x36, 0x6a, 0x4c, 0x1d, 0x76, 0xcc, 0x1c, 0x64,
-	0xfc, 0x27, 0x43, 0x70, 0x0a, 0x91, 0x9a, 0x16, 0xe8, 0x5f, 0x6d, 0xf0, 0xdf, 0x25, 0xcf, 0xf5,
-	0xac, 0xb8, 0x70, 0x9f, 0xd6, 0x31, 0xb9, 0x0f, 0x03, 0xc9, 0xd6, 0x85, 0x60, 0x4f, 0x2b, 0x45,
-	0x17, 0x15, 0x27, 0xa8, 0x2e, 0x1b, 0x1a, 0xcb, 0xdf, 0xe6, 0x29, 0xf2, 0x05, 0xb8, 0x2a, 0xda,
-	0x56, 0xee, 0x7c, 0xff, 0x96, 0x93, 0x32, 0x09, 0xa3, 0xad, 0x9c, 0xe7, 0x4a, 0x1c, 0x28, 0x4a,
-	0xf5, 0x89, 0xd9, 0x95, 0x97, 0xd9, 0xe9, 0xe5, 0x88, 0xd7, 0x19, 0x35, 0xec, 0xd9, 0x23, 0xf0,
-	0xeb, 0xa1, 0x7a, 0x81, 0x2f, 0xd9, 0xc1, 0x4c, 0xad, 0x7f, 0x75, 0xc3, 0x6e, 0x6a, 0x5f, 0xf9,
-	0xb4, 0x0c, 0x1e, 0xdb, 0x5f, 0x5b, 0xc1, 0x37, 0xe0, 0x61, 0x26, 0xbc, 0x7a, 0x92, 0x8c, 0x49,
-	0x15, 0x65, 0x3b, 0xd3, 0xea, 0x06, 0x38, 0x4e, 0x60, 0x99, 0x04, 0xd3, 0x6f, 0x01, 0xf4, 0x9b,
-	0x71, 0x85, 0x25, 0x91, 0x49, 0x75, 0x3d, 0xb7, 0x9e, 0x8a, 0xea, 0x45, 0x39, 0x6b, 0x9d, 0x19,
-	0x73, 0x85, 0xcd, 0xba, 0xbf, 0x78, 0xf8, 0x08, 0xad, 0x3a, 0xf8, 0xf9, 0xf2, 0x55, 0x00, 0x00,
-	0x00, 0xff, 0xff, 0x58, 0xae, 0x00, 0xd6, 0xa0, 0x06, 0x00, 0x00,
-}

+ 0 - 98
pkg/tsdb/models/tsdb_plugin.proto

@@ -1,98 +0,0 @@
-syntax = "proto3";
-option go_package = "proto";
-
-package plugins;
-
-message TsdbQuery {
-  TimeRange timeRange = 1;
-  DatasourceInfo datasource = 2;
-  repeated Query queries = 3;
-}
-
-message Query {
-  string refId = 1;
-  int64 maxDataPoints = 2;
-  int64 intervalMs = 3;
-  string modelJson = 4;
-}
-
-message TimeRange {
-  string fromRaw = 1;
-  string toRaw = 2;
-  int64 fromEpochMs = 3;
-  int64 toEpochMs = 4;
-}
-
-message Response {
-  repeated QueryResult results = 1;
-}
-
-message QueryResult {
-  string error = 1;
-  string refId = 2;
-  string metaJson = 3;
-  repeated TimeSeries series = 4;
-  repeated Table tables = 5;
-}
-
-message Table {
-  repeated TableColumn columns = 1;
-  repeated TableRow rows = 2;
-}
-
-message TableColumn {
-  string name = 1;
-}
-
-message TableRow {
-  repeated RowValue values = 1;
-}
-
-message RowValue {
-  enum Kind {
-    // Field type null.
-    TYPE_NULL           = 0;
-    // Field type double.
-    TYPE_DOUBLE          = 1;
-    // Field type int64.
-    TYPE_INT64          = 2;
-    // Field type bool.
-    TYPE_BOOL           = 3;
-    // Field type string.
-    TYPE_STRING         = 4;
-    // Field type bytes.
-    TYPE_BYTES          = 5;
-  };
-
-  Kind kind = 1;
-  double doubleValue = 2;
-  int64 int64Value = 3;
-  bool boolValue = 4;
-  string stringValue = 5;
-  bytes bytesValue = 6;
-}
-
-message DatasourceInfo {
-  int64 id = 1;
-  int64 orgId = 2;
-  string name = 3;
-  string type = 4;
-  string url = 5;
-  string jsonData = 6;
-  string secureJsonData = 7;
-}
-
-message TimeSeries {
-  string name = 1;
-  map<string, string> tags = 2;
-  repeated Point points = 3;
-}
-
-message Point {
-  int64 timestamp = 1;
-  double value = 2;
-}
-
-service TsdbPlugin {
-    rpc Query(TsdbQuery) returns (Response);
-}

+ 71 - 59
pkg/tsdb/mysql/mysql.go

@@ -5,8 +5,8 @@ import (
 	"context"
 	"context"
 	"database/sql"
 	"database/sql"
 	"fmt"
 	"fmt"
+	"reflect"
 	"strconv"
 	"strconv"
-
 	"time"
 	"time"
 
 
 	"github.com/go-sql-driver/mysql"
 	"github.com/go-sql-driver/mysql"
@@ -73,24 +73,36 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
 		table.Columns[i].Text = name
 		table.Columns[i].Text = name
 	}
 	}
 
 
-	columnTypes, err := rows.ColumnTypes()
-	if err != nil {
-		return err
-	}
-
 	rowLimit := 1000000
 	rowLimit := 1000000
 	rowCount := 0
 	rowCount := 0
+	timeIndex := -1
+
+	// check if there is a column named time
+	for i, col := range columnNames {
+		switch col {
+		case "time_sec":
+			timeIndex = i
+		}
+	}
 
 
 	for ; rows.Next(); rowCount++ {
 	for ; rows.Next(); rowCount++ {
 		if rowCount > rowLimit {
 		if rowCount > rowLimit {
 			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
 			return fmt.Errorf("MySQL query row limit exceeded, limit %d", rowLimit)
 		}
 		}
 
 
-		values, err := e.getTypedRowData(columnTypes, rows)
+		values, err := e.getTypedRowData(rows)
 		if err != nil {
 		if err != nil {
 			return err
 			return err
 		}
 		}
 
 
+		// for annotations, convert to epoch
+		if timeIndex != -1 {
+			switch value := values[timeIndex].(type) {
+			case time.Time:
+				values[timeIndex] = float64(value.UnixNano() / 1e9)
+			}
+		}
+
 		table.Rows = append(table.Rows, values)
 		table.Rows = append(table.Rows, values)
 	}
 	}
 
 
@@ -99,60 +111,20 @@ func (e MysqlQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Rows,
 	return nil
 	return nil
 }
 }
 
 
-func (e MysqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.Rows) (tsdb.RowValues, error) {
+func (e MysqlQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, error) {
+	types, err := rows.ColumnTypes()
+	if err != nil {
+		return nil, err
+	}
+
 	values := make([]interface{}, len(types))
 	values := make([]interface{}, len(types))
 
 
-	for i, stype := range types {
-		e.log.Debug("type", "type", stype)
-		switch stype.DatabaseTypeName() {
-		case mysql.FieldTypeNameTiny:
-			values[i] = new(int8)
-		case mysql.FieldTypeNameInt24:
-			values[i] = new(int32)
-		case mysql.FieldTypeNameShort:
-			values[i] = new(int16)
-		case mysql.FieldTypeNameVarString:
-			values[i] = new(string)
-		case mysql.FieldTypeNameVarChar:
-			values[i] = new(string)
-		case mysql.FieldTypeNameLong:
-			values[i] = new(int)
-		case mysql.FieldTypeNameLongLong:
-			values[i] = new(int64)
-		case mysql.FieldTypeNameDouble:
-			values[i] = new(float64)
-		case mysql.FieldTypeNameDecimal:
-			values[i] = new(float32)
-		case mysql.FieldTypeNameNewDecimal:
-			values[i] = new(float64)
-		case mysql.FieldTypeNameFloat:
-			values[i] = new(float64)
-		case mysql.FieldTypeNameTimestamp:
-			values[i] = new(time.Time)
-		case mysql.FieldTypeNameDateTime:
-			values[i] = new(time.Time)
-		case mysql.FieldTypeNameTime:
-			values[i] = new(string)
-		case mysql.FieldTypeNameYear:
-			values[i] = new(int16)
-		case mysql.FieldTypeNameNULL:
-			values[i] = nil
-		case mysql.FieldTypeNameBit:
+	for i := range values {
+		scanType := types[i].ScanType()
+		values[i] = reflect.New(scanType).Interface()
+
+		if types[i].DatabaseTypeName() == "BIT" {
 			values[i] = new([]byte)
 			values[i] = new([]byte)
-		case mysql.FieldTypeNameBLOB:
-			values[i] = new(string)
-		case mysql.FieldTypeNameTinyBLOB:
-			values[i] = new(string)
-		case mysql.FieldTypeNameMediumBLOB:
-			values[i] = new(string)
-		case mysql.FieldTypeNameLongBLOB:
-			values[i] = new(string)
-		case mysql.FieldTypeNameString:
-			values[i] = new(string)
-		case mysql.FieldTypeNameDate:
-			values[i] = new(string)
-		default:
-			return nil, fmt.Errorf("Database type %s not supported", stype.DatabaseTypeName())
 		}
 		}
 	}
 	}
 
 
@@ -160,14 +132,54 @@ func (e MysqlQueryEndpoint) getTypedRowData(types []*sql.ColumnType, rows *core.
 		return nil, err
 		return nil, err
 	}
 	}
 
 
+	for i := 0; i < len(types); i++ {
+		typeName := reflect.ValueOf(values[i]).Type().String()
+
+		switch typeName {
+		case "*sql.RawBytes":
+			values[i] = string(*values[i].(*sql.RawBytes))
+		case "*mysql.NullTime":
+			sqlTime := (*values[i].(*mysql.NullTime))
+			if sqlTime.Valid {
+				values[i] = sqlTime.Time
+			} else {
+				values[i] = nil
+			}
+		case "*sql.NullInt64":
+			nullInt64 := (*values[i].(*sql.NullInt64))
+			if nullInt64.Valid {
+				values[i] = nullInt64.Int64
+			} else {
+				values[i] = nil
+			}
+		case "*sql.NullFloat64":
+			nullFloat64 := (*values[i].(*sql.NullFloat64))
+			if nullFloat64.Valid {
+				values[i] = nullFloat64.Float64
+			} else {
+				values[i] = nil
+			}
+		}
+
+		if types[i].DatabaseTypeName() == "DECIMAL" {
+			f, err := strconv.ParseFloat(values[i].(string), 64)
+
+			if err == nil {
+				values[i] = f
+			} else {
+				values[i] = nil
+			}
+		}
+	}
+
 	return values, nil
 	return values, nil
 }
 }
 
 
 func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
 func (e MysqlQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error {
 	pointsBySeries := make(map[string]*tsdb.TimeSeries)
 	pointsBySeries := make(map[string]*tsdb.TimeSeries)
 	seriesByQueryOrder := list.New()
 	seriesByQueryOrder := list.New()
-	columnNames, err := rows.Columns()
 
 
+	columnNames, err := rows.Columns()
 	if err != nil {
 	if err != nil {
 		return err
 		return err
 	}
 	}

+ 43 - 32
pkg/tsdb/mysql/mysql_test.go

@@ -30,19 +30,19 @@ func TestMySQL(t *testing.T) {
 		defer sess.Close()
 		defer sess.Close()
 
 
 		sql := "CREATE TABLE `mysql_types` ("
 		sql := "CREATE TABLE `mysql_types` ("
-		sql += "`atinyint` tinyint(1),"
-		sql += "`avarchar` varchar(3),"
+		sql += "`atinyint` tinyint(1) NOT NULL,"
+		sql += "`avarchar` varchar(3) NOT NULL,"
 		sql += "`achar` char(3),"
 		sql += "`achar` char(3),"
-		sql += "`amediumint` mediumint,"
-		sql += "`asmallint` smallint,"
-		sql += "`abigint` bigint,"
-		sql += "`aint` int(11),"
+		sql += "`amediumint` mediumint NOT NULL,"
+		sql += "`asmallint` smallint NOT NULL,"
+		sql += "`abigint` bigint NOT NULL,"
+		sql += "`aint` int(11) NOT NULL,"
 		sql += "`adouble` double(10,2),"
 		sql += "`adouble` double(10,2),"
 		sql += "`anewdecimal` decimal(10,2),"
 		sql += "`anewdecimal` decimal(10,2),"
-		sql += "`afloat` float(10,2),"
+		sql += "`afloat` float(10,2) NOT NULL,"
 		sql += "`atimestamp` timestamp NOT NULL,"
 		sql += "`atimestamp` timestamp NOT NULL,"
-		sql += "`adatetime` datetime,"
-		sql += "`atime` time,"
+		sql += "`adatetime` datetime NOT NULL,"
+		sql += "`atime` time NOT NULL,"
 		// sql += "`ayear` year," // Crashes xorm when running cleandb
 		// sql += "`ayear` year," // Crashes xorm when running cleandb
 		sql += "`abit` bit(1),"
 		sql += "`abit` bit(1),"
 		sql += "`atinytext` tinytext,"
 		sql += "`atinytext` tinytext,"
@@ -55,7 +55,12 @@ func TestMySQL(t *testing.T) {
 		sql += "`alongblob` longblob,"
 		sql += "`alongblob` longblob,"
 		sql += "`aenum` enum('val1', 'val2'),"
 		sql += "`aenum` enum('val1', 'val2'),"
 		sql += "`aset` set('a', 'b', 'c', 'd'),"
 		sql += "`aset` set('a', 'b', 'c', 'd'),"
-		sql += "`adate` date"
+		sql += "`adate` date,"
+		sql += "`time_sec` datetime(6),"
+		sql += "`aintnull` int(11),"
+		sql += "`afloatnull` float(10,2),"
+		sql += "`avarcharnull` varchar(3),"
+		sql += "`adecimalnull` decimal(10,2)"
 		sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
 		sql += ") ENGINE=InnoDB DEFAULT CHARSET=latin1;"
 		_, err := sess.Exec(sql)
 		_, err := sess.Exec(sql)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
@@ -64,11 +69,11 @@ func TestMySQL(t *testing.T) {
 		sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
 		sql += "(`atinyint`, `avarchar`, `achar`, `amediumint`, `asmallint`, `abigint`, `aint`, `adouble`, "
 		sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, "
 		sql += "`anewdecimal`, `afloat`, `adatetime`, `atimestamp`, `atime`, `abit`, `atinytext`, "
 		sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
 		sql += "`atinyblob`, `atext`, `ablob`, `amediumtext`, `amediumblob`, `alongtext`, `alongblob`, "
-		sql += "`aenum`, `aset`, `adate`) "
+		sql += "`aenum`, `aset`, `adate`, `time_sec`) "
 		sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
 		sql += "VALUES(1, 'abc', 'def', 1, 10, 100, 1420070400, 1.11, "
 		sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', "
 		sql += "2.22, 3.33, now(), current_timestamp(), '11:11:11', 1, 'tinytext', "
 		sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
 		sql += "'tinyblob', 'text', 'blob', 'mediumtext', 'mediumblob', 'longtext', 'longblob', "
-		sql += "'val2', 'a,b', curdate());"
+		sql += "'val2', 'a,b', curdate(), '2018-01-01 00:01:01.123456');"
 		_, err = sess.Exec(sql)
 		_, err = sess.Exec(sql)
 		So(err, ShouldBeNil)
 		So(err, ShouldBeNil)
 
 
@@ -90,32 +95,38 @@ func TestMySQL(t *testing.T) {
 			So(err, ShouldBeNil)
 			So(err, ShouldBeNil)
 
 
 			column := queryResult.Tables[0].Rows[0]
 			column := queryResult.Tables[0].Rows[0]
+
 			So(*column[0].(*int8), ShouldEqual, 1)
 			So(*column[0].(*int8), ShouldEqual, 1)
-			So(*column[1].(*string), ShouldEqual, "abc")
-			So(*column[2].(*string), ShouldEqual, "def")
+			So(column[1].(string), ShouldEqual, "abc")
+			So(column[2].(string), ShouldEqual, "def")
 			So(*column[3].(*int32), ShouldEqual, 1)
 			So(*column[3].(*int32), ShouldEqual, 1)
 			So(*column[4].(*int16), ShouldEqual, 10)
 			So(*column[4].(*int16), ShouldEqual, 10)
 			So(*column[5].(*int64), ShouldEqual, 100)
 			So(*column[5].(*int64), ShouldEqual, 100)
-			So(*column[6].(*int), ShouldEqual, 1420070400)
-			So(*column[7].(*float64), ShouldEqual, 1.11)
-			So(*column[8].(*float64), ShouldEqual, 2.22)
-			So(*column[9].(*float64), ShouldEqual, 3.33)
+			So(*column[6].(*int32), ShouldEqual, 1420070400)
+			So(column[7].(float64), ShouldEqual, 1.11)
+			So(column[8].(float64), ShouldEqual, 2.22)
+			So(*column[9].(*float32), ShouldEqual, 3.33)
 			_, offset := time.Now().Zone()
 			_, offset := time.Now().Zone()
-			So((*column[10].(*time.Time)), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
-			So(*column[11].(*time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
-			So(*column[12].(*string), ShouldEqual, "11:11:11")
+			So(column[10].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
+			So(column[11].(time.Time), ShouldHappenWithin, time.Duration(10*time.Second), time.Now().Add(time.Duration(offset)*time.Second))
+			So(column[12].(string), ShouldEqual, "11:11:11")
 			So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
 			So(*column[13].(*[]byte), ShouldHaveSameTypeAs, []byte{1})
-			So(*column[14].(*string), ShouldEqual, "tinytext")
-			So(*column[15].(*string), ShouldEqual, "tinyblob")
-			So(*column[16].(*string), ShouldEqual, "text")
-			So(*column[17].(*string), ShouldEqual, "blob")
-			So(*column[18].(*string), ShouldEqual, "mediumtext")
-			So(*column[19].(*string), ShouldEqual, "mediumblob")
-			So(*column[20].(*string), ShouldEqual, "longtext")
-			So(*column[21].(*string), ShouldEqual, "longblob")
-			So(*column[22].(*string), ShouldEqual, "val2")
-			So(*column[23].(*string), ShouldEqual, "a,b")
-			So(*column[24].(*string), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
+			So(column[14].(string), ShouldEqual, "tinytext")
+			So(column[15].(string), ShouldEqual, "tinyblob")
+			So(column[16].(string), ShouldEqual, "text")
+			So(column[17].(string), ShouldEqual, "blob")
+			So(column[18].(string), ShouldEqual, "mediumtext")
+			So(column[19].(string), ShouldEqual, "mediumblob")
+			So(column[20].(string), ShouldEqual, "longtext")
+			So(column[21].(string), ShouldEqual, "longblob")
+			So(column[22].(string), ShouldEqual, "val2")
+			So(column[23].(string), ShouldEqual, "a,b")
+			So(column[24].(time.Time).Format("2006-01-02T00:00:00Z"), ShouldEqual, time.Now().Format("2006-01-02T00:00:00Z"))
+			So(column[25].(float64), ShouldEqual, 1514764861)
+			So(column[26], ShouldEqual, nil)
+			So(column[27], ShouldEqual, nil)
+			So(column[28], ShouldEqual, "")
+			So(column[29], ShouldEqual, nil)
 		})
 		})
 	})
 	})
 }
 }

+ 6 - 0
public/app/core/angular_wrappers.ts

@@ -5,6 +5,7 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import LoginBackground from './components/Login/LoginBackground';
 import LoginBackground from './components/Login/LoginBackground';
 import { SearchResult } from './components/search/SearchResult';
 import { SearchResult } from './components/search/SearchResult';
 import UserPicker from './components/UserPicker/UserPicker';
 import UserPicker from './components/UserPicker/UserPicker';
+import { TagFilter } from './components/TagFilter/TagFilter';
 
 
 export function registerAngularDirectives() {
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@@ -13,4 +14,9 @@ export function registerAngularDirectives() {
   react2AngularDirective('loginBackground', LoginBackground, []);
   react2AngularDirective('loginBackground', LoginBackground, []);
   react2AngularDirective('searchResult', SearchResult, []);
   react2AngularDirective('searchResult', SearchResult, []);
   react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']);
   react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'teamId', 'refreshList']);
+  react2AngularDirective('tagFilter', TagFilter, [
+    'tags',
+    ['onSelect', { watchDepth: 'reference' }],
+    ['tagOptions', { watchDepth: 'reference' }],
+  ]);
 }
 }

+ 37 - 0
public/app/core/components/TagFilter/TagBadge.tsx

@@ -0,0 +1,37 @@
+import React from 'react';
+import tags from 'app/core/utils/tags';
+
+export interface IProps {
+  label: string;
+  removeIcon: boolean;
+  count: number;
+  onClick: any;
+}
+
+export class TagBadge extends React.Component<IProps, any> {
+  constructor(props) {
+    super(props);
+    this.onClick = this.onClick.bind(this);
+  }
+
+  onClick(event) {
+    this.props.onClick(event);
+  }
+
+  render() {
+    const { label, removeIcon, count } = this.props;
+    const { color, borderColor } = tags.getTagColorsFromName(label);
+    const tagStyle = {
+      backgroundColor: color,
+      borderColor: borderColor,
+    };
+    const countLabel = count !== 0 && <span className="tag-count-label">{`(${count})`}</span>;
+
+    return (
+      <span className={`label label-tag`} onClick={this.onClick} style={tagStyle}>
+        {removeIcon && <i className="fa fa-remove" />}
+        {label} {countLabel}
+      </span>
+    );
+  }
+}

+ 69 - 0
public/app/core/components/TagFilter/TagFilter.tsx

@@ -0,0 +1,69 @@
+import _ from 'lodash';
+import React from 'react';
+import { Async } from 'react-select';
+import { TagValue } from './TagValue';
+import { TagOption } from './TagOption';
+
+export interface IProps {
+  tags: string[];
+  tagOptions: () => any;
+  onSelect: (tag: string) => void;
+}
+
+export class TagFilter extends React.Component<IProps, any> {
+  inlineTags: boolean;
+
+  constructor(props) {
+    super(props);
+
+    this.searchTags = this.searchTags.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.onTagRemove = this.onTagRemove.bind(this);
+  }
+
+  searchTags(query) {
+    return this.props.tagOptions().then(options => {
+      const tags = _.map(options, tagOption => {
+        return { value: tagOption.term, label: tagOption.term, count: tagOption.count };
+      });
+      return { options: tags };
+    });
+  }
+
+  onChange(newTags) {
+    this.props.onSelect(newTags);
+  }
+
+  onTagRemove(tag) {
+    let newTags = _.without(this.props.tags, tag.label);
+    newTags = _.map(newTags, tag => {
+      return { value: tag };
+    });
+    this.props.onSelect(newTags);
+  }
+
+  render() {
+    let selectOptions = {
+      loadOptions: this.searchTags,
+      onChange: this.onChange,
+      value: this.props.tags,
+      multi: true,
+      className: 'gf-form-input gf-form-input--form-dropdown',
+      placeholder: 'Tags',
+      loadingPlaceholder: 'Loading...',
+      noResultsText: 'No tags found',
+      optionComponent: TagOption,
+    };
+
+    selectOptions['valueComponent'] = TagValue;
+
+    return (
+      <div className="gf-form gf-form--has-input-icon gf-form--grow">
+        <div className="tag-filter">
+          <Async {...selectOptions} />
+        </div>
+        <i className="gf-form-input-icon fa fa-tag" />
+      </div>
+    );
+  }
+}

+ 52 - 0
public/app/core/components/TagFilter/TagOption.tsx

@@ -0,0 +1,52 @@
+import React from 'react';
+import { TagBadge } from './TagBadge';
+
+export interface IProps {
+  onSelect: any;
+  onFocus: any;
+  option: any;
+  isFocused: any;
+  className: any;
+}
+
+export class TagOption extends React.Component<IProps, any> {
+  constructor(props) {
+    super(props);
+    this.handleMouseDown = this.handleMouseDown.bind(this);
+    this.handleMouseEnter = this.handleMouseEnter.bind(this);
+    this.handleMouseMove = this.handleMouseMove.bind(this);
+  }
+
+  handleMouseDown(event) {
+    event.preventDefault();
+    event.stopPropagation();
+    this.props.onSelect(this.props.option, event);
+  }
+
+  handleMouseEnter(event) {
+    this.props.onFocus(this.props.option, event);
+  }
+
+  handleMouseMove(event) {
+    if (this.props.isFocused) {
+      return;
+    }
+    this.props.onFocus(this.props.option, event);
+  }
+
+  render() {
+    const { option, className } = this.props;
+
+    return (
+      <button
+        onMouseDown={this.handleMouseDown}
+        onMouseEnter={this.handleMouseEnter}
+        onMouseMove={this.handleMouseMove}
+        title={option.title}
+        className={`tag-filter-option btn btn-link ${className || ''}`}
+      >
+        <TagBadge label={option.label} removeIcon={false} count={option.count} onClick={this.handleMouseDown} />
+      </button>
+    );
+  }
+}

+ 26 - 0
public/app/core/components/TagFilter/TagValue.tsx

@@ -0,0 +1,26 @@
+import React from 'react';
+import { TagBadge } from './TagBadge';
+
+export interface IProps {
+  value: any;
+  className: any;
+  onClick: any;
+  onRemove: any;
+}
+
+export class TagValue extends React.Component<IProps, any> {
+  constructor(props) {
+    super(props);
+    this.onClick = this.onClick.bind(this);
+  }
+
+  onClick(event) {
+    this.props.onRemove(this.props.value, event);
+  }
+
+  render() {
+    const { value } = this.props;
+
+    return <TagBadge label={value.label} removeIcon={true} count={0} onClick={this.onClick} />;
+  }
+}

+ 22 - 9
public/app/core/components/form_dropdown/form_dropdown.ts

@@ -1,9 +1,11 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import $ from 'jquery';
 import coreModule from '../../core_module';
 import coreModule from '../../core_module';
 
 
 function typeaheadMatcher(item) {
 function typeaheadMatcher(item) {
   var str = this.query;
   var str = this.query;
+  if (str === '') {
+    return true;
+  }
   if (str[0] === '/') {
   if (str[0] === '/') {
     str = str.substring(1);
     str = str.substring(1);
   }
   }
@@ -30,6 +32,8 @@ export class FormDropdownCtrl {
   getOptions: any;
   getOptions: any;
   optionCache: any;
   optionCache: any;
   lookupText: boolean;
   lookupText: boolean;
+  placeholder: any;
+  startOpen: any;
 
 
   /** @ngInject **/
   /** @ngInject **/
   constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
   constructor(private $scope, $element, private $sce, private templateSrv, private $q) {
@@ -47,6 +51,10 @@ export class FormDropdownCtrl {
       this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
       this.cssClasses = 'gf-form-input gf-form-input--dropdown ' + this.cssClass;
     }
     }
 
 
+    if (this.placeholder) {
+      this.inputElement.attr('placeholder', this.placeholder);
+    }
+
     this.inputElement.attr('data-provide', 'typeahead');
     this.inputElement.attr('data-provide', 'typeahead');
     this.inputElement.typeahead({
     this.inputElement.typeahead({
       source: this.typeaheadSource.bind(this),
       source: this.typeaheadSource.bind(this),
@@ -61,8 +69,7 @@ export class FormDropdownCtrl {
     var typeahead = this.inputElement.data('typeahead');
     var typeahead = this.inputElement.data('typeahead');
     typeahead.lookup = function() {
     typeahead.lookup = function() {
       this.query = this.$element.val() || '';
       this.query = this.$element.val() || '';
-      var items = this.source(this.query, $.proxy(this.process, this));
-      return items ? this.process(items) : items;
+      this.source(this.query, this.process.bind(this));
     };
     };
 
 
     this.linkElement.keydown(evt => {
     this.linkElement.keydown(evt => {
@@ -81,6 +88,10 @@ export class FormDropdownCtrl {
     });
     });
 
 
     this.inputElement.blur(this.inputBlur.bind(this));
     this.inputElement.blur(this.inputBlur.bind(this));
+
+    if (this.startOpen) {
+      setTimeout(this.open.bind(this), 0);
+    }
   }
   }
 
 
   getOptionsInternal(query) {
   getOptionsInternal(query) {
@@ -121,9 +132,9 @@ export class FormDropdownCtrl {
       });
       });
 
 
       // add custom values
       // add custom values
-      if (this.allowCustom) {
+      if (this.allowCustom && this.text !== '') {
         if (_.indexOf(optionTexts, this.text) === -1) {
         if (_.indexOf(optionTexts, this.text) === -1) {
-          options.unshift(this.text);
+          optionTexts.unshift(this.text);
         }
         }
       }
       }
 
 
@@ -228,10 +239,10 @@ const template = `
   style="display:none">
   style="display:none">
 </input>
 </input>
 <a ng-class="ctrl.cssClasses"
 <a ng-class="ctrl.cssClasses"
-	 tabindex="1"
-	 ng-click="ctrl.open()"
-	 give-focus="ctrl.focus"
-	 ng-bind-html="ctrl.display">
+   tabindex="1"
+   ng-click="ctrl.open()"
+   give-focus="ctrl.focus"
+   ng-bind-html="ctrl.display || '&nbsp;'">
 </a>
 </a>
 `;
 `;
 
 
@@ -250,6 +261,8 @@ export function formDropdownDirective() {
       allowCustom: '@',
       allowCustom: '@',
       labelMode: '@',
       labelMode: '@',
       lookupText: '@',
       lookupText: '@',
+      placeholder: '@',
+      startOpen: '@',
     },
     },
   };
   };
 }
 }

+ 18 - 28
public/app/core/components/search/search.html

@@ -12,8 +12,7 @@
 						ng-model-options="{ debounce: 500 }"
 						ng-model-options="{ debounce: 500 }"
 						spellcheck='false'
 						spellcheck='false'
 						ng-change="ctrl.search()"
 						ng-change="ctrl.search()"
-						ng-blur="ctrl.searchInputBlur()"
-						/>
+            />
 
 
 		<div class="search-field-spacer"></div>
 		<div class="search-field-spacer"></div>
 	</div>
 	</div>
@@ -31,37 +30,28 @@
     </div>
     </div>
 
 
     <div class="search-dropdown__col_2">
     <div class="search-dropdown__col_2">
-      <!-- <div class="search&#45;filter&#45;box"> -->
-      <!--   <div class="search&#45;filter&#45;box__header"> -->
-      <!--     <i class="fa fa&#45;filter"></i> -->
-      <!--     Filter by: -->
-      <!--     <a class="pointer pull&#45;right small"> -->
-      <!--       <i class="fa fa&#45;remove"></i> Clear -->
-      <!--     </a> -->
-      <!--   </div> -->
-      <!--  -->
-      <!--   <div class="gf&#45;form"> -->
-      <!--     <folder&#45;picker initial&#45;title="ctrl.initialFolderFilterTitle" -->
-      <!--                    on&#45;change="ctrl.onFolderChange($folder)" -->
-      <!--                    label&#45;class="width&#45;4"> -->
-      <!--     </folder&#45;picker> -->
-      <!--   </div> -->
-      <!--  -->
-      <!--   <div class="gf&#45;form"> -->
-      <!--     <label class="gf&#45;form&#45;label width&#45;4">Tags</label> -->
-      <!--     <bootstrap&#45;tagsinput ng&#45;model="ctrl.dashboard.tags" tagclass="label label&#45;tag" placeholder="add tags"> -->
-      <!--     </bootstrap&#45;tagsinput> -->
-      <!--   </div> -->
-      <!-- </div> -->
+      <div class="search-filter-box" ng-click="ctrl.onFilterboxClick()">
+        <div class="search-filter-box__header">
+          <i class="fa fa-filter"></i>
+          Filter by:
+          <a class="pointer pull-right small" ng-click="ctrl.clearSearchFilter()">
+            <i class="fa fa-remove"></i> Clear
+          </a>
+        </div>
+
+        <tag-filter tags="ctrl.query.tag" tagOptions="ctrl.getTags" onSelect="ctrl.onTagSelect">
+        </tag-filter>
+      </div>
 
 
       <div class="search-filter-box">
       <div class="search-filter-box">
         <a href="dashboard/new" class="search-filter-box-link">
         <a href="dashboard/new" class="search-filter-box-link">
-          <i class="gicon gicon-dashboard-new"></i>
-          New dashboard
+          <i class="gicon gicon-dashboard-new"></i> New dashboard
         </a>
         </a>
         <a href="dashboards/folder/new" class="search-filter-box-link">
         <a href="dashboards/folder/new" class="search-filter-box-link">
-          <i class="gicon gicon-folder-new"></i>
-          New folder
+          <i class="gicon gicon-folder-new"></i> New folder
+        </a>
+        <a href="dashboard/import" class="search-filter-box-link">
+          <i class="gicon gicon-dashboard-import"></i> Import dashboard
         </a>
         </a>
         <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
         <a class="search-filter-box-link" target="_blank" href="https://grafana.com/dashboards?utm_source=grafana_search">
           <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find  dashboards on Grafana.com
           <img src="public/img/icn-dashboard-tiny.svg" width="20" /> Find  dashboards on Grafana.com

+ 26 - 5
public/app/core/components/search/search.ts

@@ -22,6 +22,8 @@ export class SearchCtrl {
     appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
     appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope);
 
 
     this.initialFolderFilterTitle = 'All';
     this.initialFolderFilterTitle = 'All';
+    this.getTags = this.getTags.bind(this);
+    this.onTagSelect = this.onTagSelect.bind(this);
   }
   }
 
 
   closeSearch() {
   closeSearch() {
@@ -88,6 +90,19 @@ export class SearchCtrl {
     }
     }
   }
   }
 
 
+  onFilterboxClick() {
+    this.giveSearchFocus = 0;
+    this.preventClose();
+  }
+
+  preventClose() {
+    this.ignoreClose = true;
+
+    this.$timeout(() => {
+      this.ignoreClose = false;
+    }, 100);
+  }
+
   moveSelection(direction) {
   moveSelection(direction) {
     if (this.results.length === 0) {
     if (this.results.length === 0) {
       return;
       return;
@@ -160,7 +175,6 @@ export class SearchCtrl {
     if (_.indexOf(this.query.tag, tag) === -1) {
     if (_.indexOf(this.query.tag, tag) === -1) {
       this.query.tag.push(tag);
       this.query.tag.push(tag);
       this.search();
       this.search();
-      this.giveSearchFocus = this.giveSearchFocus + 1;
     }
     }
   }
   }
 
 
@@ -173,10 +187,17 @@ export class SearchCtrl {
   }
   }
 
 
   getTags() {
   getTags() {
-    return this.searchSrv.getDashboardTags().then(results => {
-      this.results = results;
-      this.giveSearchFocus = this.giveSearchFocus + 1;
-    });
+    return this.searchSrv.getDashboardTags();
+  }
+
+  onTagSelect(newTags) {
+    this.query.tag = _.map(newTags, tag => tag.value);
+    this.search();
+  }
+
+  clearSearchFilter() {
+    this.query.tag = [];
+    this.search();
   }
   }
 
 
   showStarred() {
   showStarred() {

+ 1 - 1
public/app/core/directives/dropdown_typeahead.js

@@ -12,7 +12,7 @@ function (_, $, coreModule) {
       ' class="gf-form-input input-medium tight-form-input"' +
       ' class="gf-form-input input-medium tight-form-input"' +
       ' spellcheck="false" style="display:none"></input>';
       ' spellcheck="false" style="display:none"></input>';
 
 
-    var buttonTemplate = '<a  class="gf-form-label tight-form-func dropdown-toggle"' +
+    var buttonTemplate = '<a class="gf-form-label tight-form-func dropdown-toggle"' +
       ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
       ' tabindex="1" gf-dropdown="menuItems" data-toggle="dropdown"' +
       ' data-placement="top"><i class="fa fa-plus"></i></a>';
       ' data-placement="top"><i class="fa fa-plus"></i></a>';
 
 

+ 2 - 73
public/app/core/directives/tags.ts

@@ -1,82 +1,11 @@
 import angular from 'angular';
 import angular from 'angular';
 import $ from 'jquery';
 import $ from 'jquery';
 import coreModule from '../core_module';
 import coreModule from '../core_module';
+import tags from 'app/core/utils/tags';
 import 'vendor/tagsinput/bootstrap-tagsinput.js';
 import 'vendor/tagsinput/bootstrap-tagsinput.js';
 
 
-function djb2(str) {
-  var hash = 5381;
-  for (var i = 0; i < str.length; i++) {
-    hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */
-  }
-  return hash;
-}
-
 function setColor(name, element) {
 function setColor(name, element) {
-  var hash = djb2(name.toLowerCase());
-  var colors = [
-    '#E24D42',
-    '#1F78C1',
-    '#BA43A9',
-    '#705DA0',
-    '#466803',
-    '#508642',
-    '#447EBC',
-    '#C15C17',
-    '#890F02',
-    '#757575',
-    '#0A437C',
-    '#6D1F62',
-    '#584477',
-    '#629E51',
-    '#2F4F4F',
-    '#BF1B00',
-    '#806EB7',
-    '#8a2eb8',
-    '#699e00',
-    '#000000',
-    '#3F6833',
-    '#2F575E',
-    '#99440A',
-    '#E0752D',
-    '#0E4AB4',
-    '#58140C',
-    '#052B51',
-    '#511749',
-    '#3F2B5B',
-  ];
-  var borderColors = [
-    '#FF7368',
-    '#459EE7',
-    '#E069CF',
-    '#9683C6',
-    '#6C8E29',
-    '#76AC68',
-    '#6AA4E2',
-    '#E7823D',
-    '#AF3528',
-    '#9B9B9B',
-    '#3069A2',
-    '#934588',
-    '#7E6A9D',
-    '#88C477',
-    '#557575',
-    '#E54126',
-    '#A694DD',
-    '#B054DE',
-    '#8FC426',
-    '#262626',
-    '#658E59',
-    '#557D84',
-    '#BF6A30',
-    '#FF9B53',
-    '#3470DA',
-    '#7E3A32',
-    '#2B5177',
-    '#773D6F',
-    '#655181',
-  ];
-  var color = colors[Math.abs(hash % colors.length)];
-  var borderColor = borderColors[Math.abs(hash % borderColors.length)];
+  const { color, borderColor } = tags.getTagColorsFromName(name);
   element.css('background-color', color);
   element.css('background-color', color);
   element.css('border-color', borderColor);
   element.css('border-color', borderColor);
 }
 }

+ 0 - 4
public/app/core/services/segment_srv.js

@@ -106,10 +106,6 @@ function (angular, _, coreModule) {
       return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
       return new MetricSegment({fake: true, html: '<i class="fa fa-plus "></i>', type: 'plus-button', cssClass: 'query-part' });
     };
     };
 
 
-    this.newSelectTagValue = function() {
-      return new MetricSegment({value: 'select tag value', fake: true});
-    };
-
   });
   });
 
 
 });
 });

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

@@ -493,6 +493,7 @@ kbn.valueFormats.kvolt = kbn.formatBuilders.decimalSIPrefix('V', 1);
 kbn.valueFormats.mvolt = kbn.formatBuilders.decimalSIPrefix('V', -1);
 kbn.valueFormats.mvolt = kbn.formatBuilders.decimalSIPrefix('V', -1);
 kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm');
 kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm');
 kbn.valueFormats.ohm = kbn.formatBuilders.decimalSIPrefix('Ω');
 kbn.valueFormats.ohm = kbn.formatBuilders.decimalSIPrefix('Ω');
+kbn.valueFormats.lumens = kbn.formatBuilders.decimalSIPrefix('Lm');
 
 
 // Temperature
 // Temperature
 kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C');
 kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C');
@@ -958,6 +959,7 @@ kbn.getUnitFormats = function() {
         { text: 'Millivolt (mV)', value: 'mvolt' },
         { text: 'Millivolt (mV)', value: 'mvolt' },
         { text: 'Decibel-milliwatt (dBm)', value: 'dBm' },
         { text: 'Decibel-milliwatt (dBm)', value: 'dBm' },
         { text: 'Ohm (Ω)', value: 'ohm' },
         { text: 'Ohm (Ω)', value: 'ohm' },
+        { text: 'Lumens (Lm)', value: 'lumens' },
       ],
       ],
     },
     },
     {
     {

+ 86 - 0
public/app/core/utils/tags.ts

@@ -0,0 +1,86 @@
+const TAG_COLORS = [
+  '#E24D42',
+  '#1F78C1',
+  '#BA43A9',
+  '#705DA0',
+  '#466803',
+  '#508642',
+  '#447EBC',
+  '#C15C17',
+  '#890F02',
+  '#757575',
+  '#0A437C',
+  '#6D1F62',
+  '#584477',
+  '#629E51',
+  '#2F4F4F',
+  '#BF1B00',
+  '#806EB7',
+  '#8a2eb8',
+  '#699e00',
+  '#000000',
+  '#3F6833',
+  '#2F575E',
+  '#99440A',
+  '#E0752D',
+  '#0E4AB4',
+  '#58140C',
+  '#052B51',
+  '#511749',
+  '#3F2B5B',
+];
+
+const TAG_BORDER_COLORS = [
+  '#FF7368',
+  '#459EE7',
+  '#E069CF',
+  '#9683C6',
+  '#6C8E29',
+  '#76AC68',
+  '#6AA4E2',
+  '#E7823D',
+  '#AF3528',
+  '#9B9B9B',
+  '#3069A2',
+  '#934588',
+  '#7E6A9D',
+  '#88C477',
+  '#557575',
+  '#E54126',
+  '#A694DD',
+  '#B054DE',
+  '#8FC426',
+  '#262626',
+  '#658E59',
+  '#557D84',
+  '#BF6A30',
+  '#FF9B53',
+  '#3470DA',
+  '#7E3A32',
+  '#2B5177',
+  '#773D6F',
+  '#655181',
+];
+
+/**
+ * Returns tag badge background and border colors based on hashed tag name.
+ * @param name tag name
+ */
+export function getTagColorsFromName(name: string): { color: string; borderColor: string } {
+  let hash = djb2(name.toLowerCase());
+  let color = TAG_COLORS[Math.abs(hash % TAG_COLORS.length)];
+  let borderColor = TAG_BORDER_COLORS[Math.abs(hash % TAG_BORDER_COLORS.length)];
+  return { color, borderColor };
+}
+
+function djb2(str) {
+  let hash = 5381;
+  for (var i = 0; i < str.length; i++) {
+    hash = (hash << 5) + hash + str.charCodeAt(i); /* hash * 33 + c */
+  }
+  return hash;
+}
+
+export default {
+  getTagColorsFromName,
+};

+ 52 - 0
public/app/core/utils/url.ts

@@ -0,0 +1,52 @@
+/**
+ * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT
+ */
+
+export function toUrlParams(a) {
+  let s = [];
+  let rbracket = /\[\]$/;
+
+  let isArray = function(obj) {
+    return Object.prototype.toString.call(obj) === '[object Array]';
+  };
+
+  let add = function(k, v) {
+    v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v;
+    s[s.length] = encodeURIComponent(k) + '=' + encodeURIComponent(v);
+  };
+
+  let buildParams = function(prefix, obj) {
+    var i, len, key;
+
+    if (prefix) {
+      if (isArray(obj)) {
+        for (i = 0, len = obj.length; i < len; i++) {
+          if (rbracket.test(prefix)) {
+            add(prefix, obj[i]);
+          } else {
+            buildParams(prefix, obj[i]);
+          }
+        }
+      } else if (obj && String(obj) === '[object Object]') {
+        for (key in obj) {
+          buildParams(prefix + '[' + key + ']', obj[key]);
+        }
+      } else {
+        add(prefix, obj);
+      }
+    } else if (isArray(obj)) {
+      for (i = 0, len = obj.length; i < len; i++) {
+        add(obj[i].name, obj[i].value);
+      }
+    } else {
+      for (key in obj) {
+        buildParams(key, obj[key]);
+      }
+    }
+    return s;
+  };
+
+  return buildParams('', a)
+    .join('&')
+    .replace(/%20/g, '+');
+}

+ 1 - 1
public/app/features/dashboard/dashboard_srv.ts

@@ -86,7 +86,7 @@ export class DashboardSrv {
 
 
   save(clone, options) {
   save(clone, options) {
     options = options || {};
     options = options || {};
-    options.folderId = this.dash.meta.folderId || clone.folderId;
+    options.folderId = options.folderId || this.dash.meta.folderId || clone.folderId;
 
 
     return this.backendSrv
     return this.backendSrv
       .saveDashboard(clone, options)
       .saveDashboard(clone, options)

+ 1 - 2
public/app/features/dashboard/dashnav/dashnav.ts

@@ -35,8 +35,7 @@ export class DashNavCtrl {
     let search = this.$location.search();
     let search = this.$location.search();
     if (search.editview) {
     if (search.editview) {
       delete search.editview;
       delete search.editview;
-    }
-    if (search.fullscreen) {
+    } else if (search.fullscreen) {
       delete search.fullscreen;
       delete search.fullscreen;
       delete search.edit;
       delete search.edit;
     }
     }

+ 5 - 13
public/app/features/dashboard/folder_picker/folder_picker.html

@@ -9,29 +9,21 @@
     </div>
     </div>
     <input type="text"
     <input type="text"
       class="gf-form-input max-width-10"
       class="gf-form-input max-width-10"
-      ng-show="ctrl.createNewFolder"
+      ng-if="ctrl.createNewFolder"
       give-focus="ctrl.createNewFolder"
       give-focus="ctrl.createNewFolder"
       ng-model="ctrl.newFolderName"
       ng-model="ctrl.newFolderName"
       ng-model-options="{ debounce: 400 }"
       ng-model-options="{ debounce: 400 }"
-      ng-class="{'validation-error': !ctrl.isNewFolderNameValid()}"
       ng-change="ctrl.newFolderNameChanged()" />
       ng-change="ctrl.newFolderNameChanged()" />
   </div>
   </div>
-  <div class="gf-form" ng-show="ctrl.createNewFolder">
-    <label class="gf-form-label text-success"
-      ng-show="ctrl.newFolderNameTouched && !ctrl.hasValidationError">
-      <i class="fa fa-check"></i>
-    </label>
-  </div>
-  <div class="gf-form" ng-show="ctrl.createNewFolder">
-    <button class="gf-form-label"
+  <div class="gf-form" ng-if="ctrl.createNewFolder">
+    <button class="btn btn-inverse"
       ng-click="ctrl.createFolder($event)"
       ng-click="ctrl.createFolder($event)"
       ng-disabled="!ctrl.newFolderNameTouched || ctrl.hasValidationError">
       ng-disabled="!ctrl.newFolderNameTouched || ctrl.hasValidationError">
       <i class="fa fa-fw fa-save"></i>&nbsp;Create
       <i class="fa fa-fw fa-save"></i>&nbsp;Create
     </button>
     </button>
   </div>
   </div>
-  <div class="gf-form" ng-show="ctrl.createNewFolder">
-    <button class="gf-form-label"
-      ng-click="ctrl.cancelCreateFolder($event)">
+  <div class="gf-form" ng-if="ctrl.createNewFolder">
+    <button class="btn btn-inverse" ng-click="ctrl.cancelCreateFolder($event)">
       Cancel
       Cancel
   </button>
   </button>
   </div>
   </div>

+ 5 - 16
public/app/features/dashboard/save_as_modal.ts

@@ -13,7 +13,7 @@ const template = `
 		</a>
 		</a>
 	</div>
 	</div>
 
 
-	<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content" novalidate>
+	<form name="ctrl.saveForm" class="modal-content" novalidate>
 		<div class="p-t-2">
 		<div class="p-t-2">
 			<div class="gf-form">
 			<div class="gf-form">
 				<label class="gf-form-label width-7">New name</label>
 				<label class="gf-form-label width-7">New name</label>
@@ -22,8 +22,6 @@ const template = `
       <div class="gf-form">
       <div class="gf-form">
         <folder-picker initial-folder-id="ctrl.folderId"
         <folder-picker initial-folder-id="ctrl.folderId"
                        on-change="ctrl.onFolderChange($folder)"
                        on-change="ctrl.onFolderChange($folder)"
-                       enter-folder-creation="ctrl.onEnterFolderCreation()"
-                       exit-folder-creation="ctrl.onExitFolderCreation()"
                        enable-create-new="true"
                        enable-create-new="true"
                        label-class="width-7">
                        label-class="width-7">
         </folder-picker>
         </folder-picker>
@@ -31,7 +29,7 @@ const template = `
 		</div>
 		</div>
 
 
 		<div class="gf-form-button-row text-center">
 		<div class="gf-form-button-row text-center">
-			<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid || !ctrl.isValidFolderSelection">Save</button>
+			<button type="submit" class="btn btn-success" ng-click="ctrl.save()">Save</button>
 			<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
 			<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a>
 		</div>
 		</div>
 	</form>
 	</form>
@@ -41,7 +39,6 @@ const template = `
 export class SaveDashboardAsModalCtrl {
 export class SaveDashboardAsModalCtrl {
   clone: any;
   clone: any;
   folderId: any;
   folderId: any;
-  isValidFolderSelection = true;
   dismiss: () => void;
   dismiss: () => void;
 
 
   /** @ngInject */
   /** @ngInject */
@@ -69,25 +66,17 @@ export class SaveDashboardAsModalCtrl {
   }
   }
 
 
   save() {
   save() {
-    return this.dashboardSrv.save(this.clone).then(this.dismiss);
-  }
-
-  onEnterFolderCreation() {
-    this.isValidFolderSelection = false;
-  }
-
-  onExitFolderCreation() {
-    this.isValidFolderSelection = true;
+    return this.dashboardSrv.save(this.clone, { folderId: this.folderId }).then(this.dismiss);
   }
   }
 
 
   keyDown(evt) {
   keyDown(evt) {
-    if (this.isValidFolderSelection && evt.keyCode === 13) {
+    if (evt.keyCode === 13) {
       this.save();
       this.save();
     }
     }
   }
   }
 
 
   onFolderChange(folder) {
   onFolderChange(folder) {
-    this.clone.folderId = folder.id;
+    this.folderId = folder.id;
   }
   }
 }
 }
 
 

+ 3 - 1
public/app/features/dashboard/settings/settings.html

@@ -10,11 +10,13 @@
 	</a>
 	</a>
 
 
 	<div class="dashboard-settings__aside-actions">
 	<div class="dashboard-settings__aside-actions">
+    <button class="btn btn-success" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
+			<i class="fa fa-save"></i> Save
+		</button>
 		<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
 		<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
 			<i class="fa fa-copy"></i>
 			<i class="fa fa-copy"></i>
 			Save As...
 			Save As...
 		</button>
 		</button>
-
 		<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
 		<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
 			<i class="fa fa-trash"></i>
 			<i class="fa fa-trash"></i>
 			Delete
 			Delete

+ 6 - 0
public/app/features/dashboard/settings/settings.ts

@@ -11,6 +11,7 @@ export class SettingsCtrl {
   json: string;
   json: string;
   alertCount: number;
   alertCount: number;
   canSaveAs: boolean;
   canSaveAs: boolean;
+  canSave: boolean;
   canDelete: boolean;
   canDelete: boolean;
   sections: any[];
   sections: any[];
 
 
@@ -26,6 +27,7 @@ export class SettingsCtrl {
     });
     });
 
 
     this.canSaveAs = contextSrv.isEditor;
     this.canSaveAs = contextSrv.isEditor;
+    this.canSave = this.dashboard.meta.canSave;
     this.canDelete = this.dashboard.meta.canSave;
     this.canDelete = this.dashboard.meta.canSave;
 
 
     this.buildSectionList();
     this.buildSectionList();
@@ -125,6 +127,10 @@ export class SettingsCtrl {
     this.dashboardSrv.showSaveAsModal();
     this.dashboardSrv.showSaveAsModal();
   }
   }
 
 
+  saveDashboard() {
+    this.dashboardSrv.saveDashboard();
+  }
+
   hideSettings() {
   hideSettings() {
     var urlParams = this.$location.search();
     var urlParams = this.$location.search();
     delete urlParams.editview;
     delete urlParams.editview;

+ 6 - 8
public/app/features/templating/editor_ctrl.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import coreModule from 'app/core/core_module';
 import coreModule from 'app/core/core_module';
 import { variableTypes } from './variable';
 import { variableTypes } from './variable';
+import appEvents from 'app/core/app_events';
 
 
 export class VariableEditorCtrl {
 export class VariableEditorCtrl {
   /** @ngInject **/
   /** @ngInject **/
@@ -56,16 +57,13 @@ export class VariableEditorCtrl {
       }
       }
 
 
       if (!$scope.current.name.match(/^\w+$/)) {
       if (!$scope.current.name.match(/^\w+$/)) {
-        $scope.appEvent('alert-warning', [
-          'Validation',
-          'Only word and digit characters are allowed in variable names',
-        ]);
+        appEvents.emit('alert-warning', ['Validation', 'Only word and digit characters are allowed in variable names']);
         return false;
         return false;
       }
       }
 
 
       var sameName = _.find($scope.variables, { name: $scope.current.name });
       var sameName = _.find($scope.variables, { name: $scope.current.name });
       if (sameName && sameName !== $scope.current) {
       if (sameName && sameName !== $scope.current) {
-        $scope.appEvent('alert-warning', ['Validation', 'Variable with the same name already exists']);
+        appEvents.emit('alert-warning', ['Validation', 'Variable with the same name already exists']);
         return false;
         return false;
       }
       }
 
 
@@ -73,7 +71,7 @@ export class VariableEditorCtrl {
         $scope.current.type === 'query' &&
         $scope.current.type === 'query' &&
         $scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
         $scope.current.query.match(new RegExp('\\$' + $scope.current.name + '(/| |$)'))
       ) {
       ) {
-        $scope.appEvent('alert-warning', [
+        appEvents.emit('alert-warning', [
           'Validation',
           'Validation',
           'Query cannot contain a reference to itself. Variable: $' + $scope.current.name,
           'Query cannot contain a reference to itself. Variable: $' + $scope.current.name,
         ]);
         ]);
@@ -96,11 +94,11 @@ export class VariableEditorCtrl {
     };
     };
 
 
     $scope.runQuery = function() {
     $scope.runQuery = function() {
-      return variableSrv.updateOptions($scope.current).then(null, function(err) {
+      return variableSrv.updateOptions($scope.current).catch(err => {
         if (err.data && err.data.message) {
         if (err.data && err.data.message) {
           err.message = err.data.message;
           err.message = err.data.message;
         }
         }
-        $scope.appEvent('alert-error', ['Templating', 'Template variables could not be initialized: ' + err.message]);
+        appEvents.emit('alert-error', ['Templating', 'Template variables could not be initialized: ' + err.message]);
       });
       });
     };
     };
 
 

+ 4 - 4
public/app/features/templating/partials/editor.html

@@ -16,12 +16,12 @@
 					Add variable
 					Add variable
 				</a>
 				</a>
 				<div class="grafana-info-box">
 				<div class="grafana-info-box">
-					<h5>What does variables do?</h5>
-					<p>Variables enables more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
+					<h5>What do variables do?</h5>
+					<p>Variables enable more interactive and dynamic dashboards. Instead of hard-coding things like server or sensor names
 					in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 					in your metric queries you can use variables in their place. Variables are shown as dropdown select boxes at the top of
 					the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 					the dashboard. These dropdowns make it easy to change the data being displayed in your dashboard.
 
 
-					Checkout the
+					Check out the
 					<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
 					<a class="external-link" href="http://docs.grafana.org/reference/templating/" target="_blank">
 						Templating documentation
 						Templating documentation
 					</a> for more information.
 					</a> for more information.
@@ -93,7 +93,7 @@
 			</div>
 			</div>
 
 
 			<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
 			<div class="gf-form" ng-show="ctrl.form.name.$error.pattern">
-				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__' that's reserved for Grafanas global variables</span>
+				<span class="gf-form-label gf-form-label--error">Template names cannot begin with '__', that's reserved for Grafana's global variables</span>
 			</div>
 			</div>
 
 
 			<div class="gf-form-inline">
 			<div class="gf-form-inline">

+ 40 - 0
public/app/features/templating/specs/editor_ctrl.jest.ts

@@ -0,0 +1,40 @@
+import { VariableEditorCtrl } from '../editor_ctrl';
+
+let mockEmit;
+jest.mock('app/core/app_events', () => {
+  mockEmit = jest.fn();
+  return {
+    emit: mockEmit,
+  };
+});
+
+describe('VariableEditorCtrl', () => {
+  let scope = {
+    runQuery: () => {
+      return Promise.resolve({});
+    },
+  };
+
+  describe('When running a variable query and the data source returns an error', () => {
+    beforeEach(() => {
+      const variableSrv = {
+        updateOptions: () => {
+          return Promise.reject({
+            data: { message: 'error' },
+          });
+        },
+      };
+
+      return new VariableEditorCtrl(scope, {}, variableSrv, {});
+    });
+
+    it('should emit an error', () => {
+      return scope.runQuery().then(res => {
+        expect(mockEmit).toBeCalled();
+        expect(mockEmit.mock.calls[0][0]).toBe('alert-error');
+        expect(mockEmit.mock.calls[0][1][0]).toBe('Templating');
+        expect(mockEmit.mock.calls[0][1][1]).toBe('Template variables could not be initialized: error');
+      });
+    });
+  });
+});

+ 96 - 54
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -1,46 +1,37 @@
-define([
-  'angular',
-  'lodash',
-  'jquery',
-  './gfunc',
-],
-function (angular, _, $, gfunc) {
+define(['angular', 'lodash', 'jquery', 'rst2html', 'tether-drop'], function(angular, _, $, rst2html, Drop) {
   'use strict';
   'use strict';
 
 
-  gfunc = gfunc.default;
+  angular.module('grafana.directives').directive('graphiteAddFunc', function($compile) {
+    var inputTemplate =
+      '<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
 
 
-  angular
-    .module('grafana.directives')
-    .directive('graphiteAddFunc', function($compile) {
-      var inputTemplate = '<input type="text"'+
-                            ' class="gf-form-input"' +
-                            ' spellcheck="false" style="display:none"></input>';
+    var buttonTemplate =
+      '<a class="gf-form-label query-part dropdown-toggle"' +
+      ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
+      '<i class="fa fa-plus"></i></a>';
 
 
-      var buttonTemplate = '<a  class="gf-form-label query-part dropdown-toggle"' +
-                              ' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
-                              '<i class="fa fa-plus"></i></a>';
+    return {
+      link: function($scope, elem) {
+        var ctrl = $scope.ctrl;
 
 
-      return {
-        link: function($scope, elem) {
-          var ctrl = $scope.ctrl;
-          var graphiteVersion = ctrl.datasource.graphiteVersion;
-          var categories = gfunc.getCategories(graphiteVersion);
-          var allFunctions = getAllFunctionNames(categories);
+        var $input = $(inputTemplate);
+        var $button = $(buttonTemplate);
 
 
-          $scope.functionMenu = createFunctionDropDownMenu(categories);
+        $input.appendTo(elem);
+        $button.appendTo(elem);
 
 
-          var $input = $(inputTemplate);
-          var $button = $(buttonTemplate);
-          $input.appendTo(elem);
-          $button.appendTo(elem);
+        ctrl.datasource.getFuncDefs().then(function(funcDefs) {
+          var allFunctions = _.map(funcDefs, 'name').sort();
+
+          $scope.functionMenu = createFunctionDropDownMenu(funcDefs);
 
 
           $input.attr('data-provide', 'typeahead');
           $input.attr('data-provide', 'typeahead');
           $input.typeahead({
           $input.typeahead({
             source: allFunctions,
             source: allFunctions,
             minLength: 1,
             minLength: 1,
             items: 10,
             items: 10,
-            updater: function (value) {
-              var funcDef = gfunc.getFuncDef(value);
+            updater: function(value) {
+              var funcDef = ctrl.datasource.getFuncDef(value);
               if (!funcDef) {
               if (!funcDef) {
                 // try find close match
                 // try find close match
                 value = value.toLowerCase();
                 value = value.toLowerCase();
@@ -48,7 +39,9 @@ function (angular, _, $, gfunc) {
                   return funcName.toLowerCase().indexOf(value) === 0;
                   return funcName.toLowerCase().indexOf(value) === 0;
                 });
                 });
 
 
-                if (!funcDef) { return; }
+                if (!funcDef) {
+                  return;
+                }
               }
               }
 
 
               $scope.$apply(function() {
               $scope.$apply(function() {
@@ -57,7 +50,7 @@ function (angular, _, $, gfunc) {
 
 
               $input.trigger('blur');
               $input.trigger('blur');
               return '';
               return '';
-            }
+            },
           });
           });
 
 
           $button.click(function() {
           $button.click(function() {
@@ -82,32 +75,81 @@ function (angular, _, $, gfunc) {
           });
           });
 
 
           $compile(elem.contents())($scope);
           $compile(elem.contents())($scope);
-        }
-      };
-    });
+        });
+
+        var drop;
+        var cleanUpDrop = function() {
+          if (drop) {
+            drop.destroy();
+            drop = null;
+          }
+        };
+
+        $(elem)
+          .on('mouseenter', 'ul.dropdown-menu li', function() {
+            cleanUpDrop();
+
+            var funcDef;
+            try {
+              funcDef = ctrl.datasource.getFuncDef($('a', this).text());
+            } catch (e) {
+              // ignore
+            }
+
+            if (funcDef && funcDef.description) {
+              var shortDesc = funcDef.description;
+              if (shortDesc.length > 500) {
+                shortDesc = shortDesc.substring(0, 497) + '...';
+              }
 
 
-  function getAllFunctionNames(categories) {
-    return _.reduce(categories, function(list, category) {
-      _.each(category, function(func) {
-        list.push(func.name);
+              var contentElement = document.createElement('div');
+              contentElement.innerHTML = '<h4>' + funcDef.name + '</h4>' + rst2html(shortDesc);
+
+              drop = new Drop({
+                target: this,
+                content: contentElement,
+                classes: 'drop-popover',
+                openOn: 'always',
+                tetherOptions: {
+                  attachment: 'bottom left',
+                  targetAttachment: 'bottom right',
+                },
+              });
+            }
+          })
+          .on('mouseout', 'ul.dropdown-menu li', function() {
+            cleanUpDrop();
+          });
+
+        $scope.$on('$destroy', cleanUpDrop);
+      },
+    };
+  });
+
+  function createFunctionDropDownMenu(funcDefs) {
+    var categories = {};
+
+    _.forEach(funcDefs, function(funcDef) {
+      if (!funcDef.category) {
+        return;
+      }
+      if (!categories[funcDef.category]) {
+        categories[funcDef.category] = [];
+      }
+      categories[funcDef.category].push({
+        text: funcDef.name,
+        click: "ctrl.addFunction('" + funcDef.name + "')",
       });
       });
-      return list;
-    }, []);
-  }
+    });
 
 
-  function createFunctionDropDownMenu(categories) {
-    return _.map(categories, function(list, category) {
-      var submenu = _.map(list, function(value) {
+    return _.sortBy(
+      _.map(categories, function(submenu, category) {
         return {
         return {
-          text: value.name,
-          click: "ctrl.addFunction('" + value.name + "')",
+          text: category,
+          submenu: _.sortBy(submenu, 'text'),
         };
         };
-      });
-
-      return {
-        text: category,
-        submenu: submenu
-      };
-    });
+      }),
+      'text'
+    );
   }
   }
 });
 });

+ 0 - 1
public/app/plugins/datasource/graphite/config_ctrl.ts

@@ -8,7 +8,6 @@ export class GraphiteConfigCtrl {
     this.datasourceSrv = datasourceSrv;
     this.datasourceSrv = datasourceSrv;
     this.current.jsonData = this.current.jsonData || {};
     this.current.jsonData = this.current.jsonData || {};
     this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
     this.current.jsonData.graphiteVersion = this.current.jsonData.graphiteVersion || '0.9';
-
     this.autoDetectGraphiteVersion();
     this.autoDetectGraphiteVersion();
   }
   }
 
 

+ 118 - 11
public/app/plugins/datasource/graphite/datasource.ts

@@ -1,6 +1,7 @@
 import _ from 'lodash';
 import _ from 'lodash';
 import * as dateMath from 'app/core/utils/datemath';
 import * as dateMath from 'app/core/utils/datemath';
 import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
 import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
+import gfunc from './gfunc';
 
 
 /** @ngInject */
 /** @ngInject */
 export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
 export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv) {
@@ -12,6 +13,8 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
   this.cacheTimeout = instanceSettings.cacheTimeout;
   this.cacheTimeout = instanceSettings.cacheTimeout;
   this.withCredentials = instanceSettings.withCredentials;
   this.withCredentials = instanceSettings.withCredentials;
   this.render_method = instanceSettings.render_method || 'POST';
   this.render_method = instanceSettings.render_method || 'POST';
+  this.funcDefs = null;
+  this.funcDefsPromise = null;
 
 
   this.getQueryOptionsInfo = function() {
   this.getQueryOptionsInfo = function() {
     return {
     return {
@@ -200,6 +203,35 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     let options = optionalOptions || {};
     let options = optionalOptions || {};
     let interpolatedQuery = templateSrv.replace(query);
     let interpolatedQuery = templateSrv.replace(query);
 
 
+    // special handling for tag_values(<tag>[,<expression>]*), this is used for template variables
+    let matches = interpolatedQuery.match(/^tag_values\(([^,]+)((, *[^,]+)*)\)$/);
+    if (matches) {
+      const expressions = [];
+      const exprRegex = /, *([^,]+)/g;
+      let match;
+      while ((match = exprRegex.exec(matches[2])) !== null) {
+        expressions.push(match[1]);
+      }
+      options.limit = 10000;
+      return this.getTagValuesAutoComplete(expressions, matches[1], undefined, options);
+    }
+
+    // special handling for tags(<expression>[,<expression>]*), this is used for template variables
+    matches = interpolatedQuery.match(/^tags\(([^,]*)((, *[^,]+)*)\)$/);
+    if (matches) {
+      const expressions = [];
+      if (matches[1]) {
+        expressions.push(matches[1]);
+        const exprRegex = /, *([^,]+)/g;
+        let match;
+        while ((match = exprRegex.exec(matches[2])) !== null) {
+          expressions.push(match[1]);
+        }
+      }
+      options.limit = 10000;
+      return this.getTagsAutoComplete(expressions, undefined, options);
+    }
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/metrics/find',
       url: '/metrics/find',
@@ -210,7 +242,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -235,7 +267,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -255,12 +287,12 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 
 
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
-      url: '/tags/' + tag,
+      url: '/tags/' + templateSrv.replace(tag),
       // for cancellations
       // for cancellations
       requestId: options.requestId,
       requestId: options.requestId,
     };
     };
 
 
-    if (options && options.range) {
+    if (options.range) {
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.from = this.translateTime(options.range.from, false);
       httpOptions.params.until = this.translateTime(options.range.to, true);
       httpOptions.params.until = this.translateTime(options.range.to, true);
     }
     }
@@ -279,18 +311,29 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getTagsAutoComplete = (expression, tagPrefix) => {
+  this.getTagsAutoComplete = (expressions, tagPrefix, optionalOptions) => {
+    let options = optionalOptions || {};
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/tags/autoComplete/tags',
       url: '/tags/autoComplete/tags',
       params: {
       params: {
-        expr: expression,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
       },
       },
+      // for cancellations
+      requestId: options.requestId,
     };
     };
 
 
     if (tagPrefix) {
     if (tagPrefix) {
       httpOptions.params.tagPrefix = tagPrefix;
       httpOptions.params.tagPrefix = tagPrefix;
     }
     }
+    if (options.limit) {
+      httpOptions.params.limit = options.limit;
+    }
+    if (options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
 
 
     return this.doGraphiteRequest(httpOptions).then(results => {
     return this.doGraphiteRequest(httpOptions).then(results => {
       if (results.data) {
       if (results.data) {
@@ -303,19 +346,30 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getTagValuesAutoComplete = (expression, tag, valuePrefix) => {
+  this.getTagValuesAutoComplete = (expressions, tag, valuePrefix, optionalOptions) => {
+    let options = optionalOptions || {};
+
     let httpOptions: any = {
     let httpOptions: any = {
       method: 'GET',
       method: 'GET',
       url: '/tags/autoComplete/values',
       url: '/tags/autoComplete/values',
       params: {
       params: {
-        expr: expression,
-        tag: tag,
+        expr: _.map(expressions, expression => templateSrv.replace(expression)),
+        tag: templateSrv.replace(tag),
       },
       },
+      // for cancellations
+      requestId: options.requestId,
     };
     };
 
 
     if (valuePrefix) {
     if (valuePrefix) {
       httpOptions.params.valuePrefix = valuePrefix;
       httpOptions.params.valuePrefix = valuePrefix;
     }
     }
+    if (options.limit) {
+      httpOptions.params.limit = options.limit;
+    }
+    if (options.range) {
+      httpOptions.params.from = this.translateTime(options.range.from, false);
+      httpOptions.params.until = this.translateTime(options.range.to, true);
+    }
 
 
     return this.doGraphiteRequest(httpOptions).then(results => {
     return this.doGraphiteRequest(httpOptions).then(results => {
       if (results.data) {
       if (results.data) {
@@ -328,10 +382,13 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
     });
     });
   };
   };
 
 
-  this.getVersion = function() {
+  this.getVersion = function(optionalOptions) {
+    let options = optionalOptions || {};
+
     let httpOptions = {
     let httpOptions = {
       method: 'GET',
       method: 'GET',
-      url: '/version/_', // Prevent last / trimming
+      url: '/version',
+      requestId: options.requestId,
     };
     };
 
 
     return this.doGraphiteRequest(httpOptions)
     return this.doGraphiteRequest(httpOptions)
@@ -347,6 +404,52 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
       });
       });
   };
   };
 
 
+  this.createFuncInstance = function(funcDef, options?) {
+    return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
+  };
+
+  this.getFuncDef = function(name) {
+    return gfunc.getFuncDef(name, this.funcDefs);
+  };
+
+  this.waitForFuncDefsLoaded = function() {
+    return this.getFuncDefs();
+  };
+
+  this.getFuncDefs = function() {
+    if (this.funcDefsPromise !== null) {
+      return this.funcDefsPromise;
+    }
+
+    if (!supportsFunctionIndex(this.graphiteVersion)) {
+      this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+      this.funcDefsPromise = Promise.resolve(this.funcDefs);
+      return this.funcDefsPromise;
+    }
+
+    let httpOptions = {
+      method: 'GET',
+      url: '/functions',
+    };
+
+    this.funcDefsPromise = this.doGraphiteRequest(httpOptions)
+      .then(results => {
+        if (results.status !== 200 || typeof results.data !== 'object') {
+          this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+        } else {
+          this.funcDefs = gfunc.parseFuncDefs(results.data);
+        }
+        return this.funcDefs;
+      })
+      .catch(err => {
+        console.log('Fetching graphite functions error', err);
+        this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion);
+        return this.funcDefs;
+      });
+
+    return this.funcDefsPromise;
+  };
+
   this.testDatasource = function() {
   this.testDatasource = function() {
     return this.metricFindQuery('*').then(function() {
     return this.metricFindQuery('*').then(function() {
       return { status: 'success', message: 'Data source is working' };
       return { status: 'success', message: 'Data source is working' };
@@ -440,3 +543,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
 function supportsTags(version: string): boolean {
 function supportsTags(version: string): boolean {
   return isVersionGtOrEq(version, '1.1');
   return isVersionGtOrEq(version, '1.1');
 }
 }
+
+function supportsFunctionIndex(version: string): boolean {
+  return isVersionGtOrEq(version, '1.1');
+}

+ 95 - 33
public/app/plugins/datasource/graphite/func_editor.js

@@ -2,17 +2,18 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'jquery',
   'jquery',
+  'rst2html',
 ],
 ],
-function (angular, _, $) {
+function (angular, _, $, rst2html) {
   'use strict';
   'use strict';
 
 
   angular
   angular
     .module('grafana.directives')
     .module('grafana.directives')
-    .directive('graphiteFuncEditor', function($compile, templateSrv) {
+    .directive('graphiteFuncEditor', function($compile, templateSrv, popoverSrv) {
 
 
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
       var paramTemplate = '<input type="text" style="display:none"' +
       var paramTemplate = '<input type="text" style="display:none"' +
-                          ' class="input-mini tight-form-func-param"></input>';
+                          ' class="input-small tight-form-func-param"></input>';
 
 
       var funcControlsTemplate =
       var funcControlsTemplate =
          '<div class="tight-form-func-controls">' +
          '<div class="tight-form-func-controls">' +
@@ -29,19 +30,20 @@ function (angular, _, $) {
           var $funcControls = $(funcControlsTemplate);
           var $funcControls = $(funcControlsTemplate);
           var ctrl = $scope.ctrl;
           var ctrl = $scope.ctrl;
           var func = $scope.func;
           var func = $scope.func;
-          var funcDef = func.def;
           var scheduledRelink = false;
           var scheduledRelink = false;
           var paramCountAtLink = 0;
           var paramCountAtLink = 0;
+          var cancelBlur = null;
 
 
           function clickFuncParam(paramIndex) {
           function clickFuncParam(paramIndex) {
             /*jshint validthis:true */
             /*jshint validthis:true */
 
 
             var $link = $(this);
             var $link = $(this);
+            var $comma = $link.prev('.comma');
             var $input = $link.next();
             var $input = $link.next();
 
 
             $input.val(func.params[paramIndex]);
             $input.val(func.params[paramIndex]);
-            $input.css('width', ($link.width() + 16) + 'px');
 
 
+            $comma.removeClass('query-part__last');
             $link.hide();
             $link.hide();
             $input.show();
             $input.show();
             $input.focus();
             $input.focus();
@@ -68,31 +70,64 @@ function (angular, _, $) {
             }
             }
           }
           }
 
 
-          function inputBlur(paramIndex) {
+          function paramDef(index) {
+            if (index < func.def.params.length) {
+              return func.def.params[index];
+            }
+            if (_.last(func.def.params).multiple) {
+              return _.assign({}, _.last(func.def.params), {optional: true});
+            }
+            return {};
+          }
+
+          function switchToLink(inputElem, paramIndex) {
             /*jshint validthis:true */
             /*jshint validthis:true */
-            var $input = $(this);
+            var $input = $(inputElem);
+
+            clearTimeout(cancelBlur);
+            cancelBlur = null;
+
             var $link = $input.prev();
             var $link = $input.prev();
+            var $comma = $link.prev('.comma');
             var newValue = $input.val();
             var newValue = $input.val();
 
 
-            if (newValue !== '' || func.def.params[paramIndex].optional) {
-              $link.html(templateSrv.highlightVariablesAsHtml(newValue));
+            // remove optional empty params
+            if (newValue !== '' || paramDef(paramIndex).optional) {
+              func.updateParam(newValue, paramIndex);
+              $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : '&nbsp;');
+            }
 
 
-              func.updateParam($input.val(), paramIndex);
-              scheduledRelinkIfNeeded();
+            scheduledRelinkIfNeeded();
 
 
-              $scope.$apply(function() {
-                ctrl.targetChanged();
-              });
+            $scope.$apply(function() {
+              ctrl.targetChanged();
+            });
 
 
-              $input.hide();
-              $link.show();
+            if ($link.hasClass('query-part__last') && newValue === '') {
+              $comma.addClass('query-part__last');
+            } else {
+              $link.removeClass('query-part__last');
             }
             }
+
+            $input.hide();
+            $link.show();
+          }
+
+          // this = input element
+          function inputBlur(paramIndex) {
+            /*jshint validthis:true */
+            var inputElem = this;
+            // happens long before the click event on the typeahead options
+            // need to have long delay because the blur
+            cancelBlur = setTimeout(function() {
+              switchToLink(inputElem, paramIndex);
+            }, 200);
           }
           }
 
 
           function inputKeyPress(paramIndex, e) {
           function inputKeyPress(paramIndex, e) {
             /*jshint validthis:true */
             /*jshint validthis:true */
             if(e.which === 13) {
             if(e.which === 13) {
-              inputBlur.call(this, paramIndex);
+              $(this).blur();
             }
             }
           }
           }
 
 
@@ -104,8 +139,8 @@ function (angular, _, $) {
           function addTypeahead($input, paramIndex) {
           function addTypeahead($input, paramIndex) {
             $input.attr('data-provide', 'typeahead');
             $input.attr('data-provide', 'typeahead');
 
 
-            var options = funcDef.params[paramIndex].options;
-            if (funcDef.params[paramIndex].type === 'int') {
+            var options = paramDef(paramIndex).options;
+            if (paramDef(paramIndex).type === 'int') {
               options = _.map(options, function(val) { return val.toString(); });
               options = _.map(options, function(val) { return val.toString(); });
             }
             }
 
 
@@ -114,9 +149,8 @@ function (angular, _, $) {
               minLength: 0,
               minLength: 0,
               items: 20,
               items: 20,
               updater: function (value) {
               updater: function (value) {
-                setTimeout(function() {
-                  inputBlur.call($input[0], paramIndex);
-                }, 0);
+                $input.val(value);
+                switchToLink($input[0], paramIndex);
                 return value;
                 return value;
               }
               }
             });
             });
@@ -148,18 +182,34 @@ function (angular, _, $) {
             $funcControls.appendTo(elem);
             $funcControls.appendTo(elem);
             $funcLink.appendTo(elem);
             $funcLink.appendTo(elem);
 
 
-            _.each(funcDef.params, function(param, index) {
-              if (param.optional && func.params.length <= index) {
-                return;
+            var defParams = _.clone(func.def.params);
+            var lastParam = _.last(func.def.params);
+
+            while (func.params.length >= defParams.length && lastParam && lastParam.multiple) {
+              defParams.push(_.assign({}, lastParam, {optional: true}));
+            }
+
+            _.each(defParams, function(param, index) {
+              if (param.optional && func.params.length < index) {
+                return false;
+              }
+
+              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
+
+              var last = (index >= func.params.length - 1) && param.optional && !paramValue;
+              if (last && param.multiple) {
+                paramValue = '+';
               }
               }
 
 
               if (index > 0) {
               if (index > 0) {
-                $('<span>, </span>').appendTo(elem);
+                $('<span class="comma' + (last ? ' query-part__last' : '') + '">, </span>').appendTo(elem);
               }
               }
 
 
-              var paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]);
-              var $paramLink = $('<a ng-click="" class="graphite-func-param-link">' + paramValue + '</a>');
+              var $paramLink = $(
+                '<a ng-click="" class="graphite-func-param-link' + (last ? ' query-part__last' : '') + '">'
+                + (paramValue || '&nbsp;') + '</a>');
               var $input = $(paramTemplate);
               var $input = $(paramTemplate);
+              $input.attr('placeholder', param.name);
 
 
               paramCountAtLink++;
               paramCountAtLink++;
 
 
@@ -171,10 +221,9 @@ function (angular, _, $) {
               $input.keypress(_.partial(inputKeyPress, index));
               $input.keypress(_.partial(inputKeyPress, index));
               $paramLink.click(_.partial(clickFuncParam, index));
               $paramLink.click(_.partial(clickFuncParam, index));
 
 
-              if (funcDef.params[index].options) {
+              if (param.options) {
                 addTypeahead($input, index);
                 addTypeahead($input, index);
               }
               }
-
             });
             });
 
 
             $('<span>)</span>').appendTo(elem);
             $('<span>)</span>').appendTo(elem);
@@ -182,7 +231,7 @@ function (angular, _, $) {
             $compile(elem.contents())($scope);
             $compile(elem.contents())($scope);
           }
           }
 
 
-          function ifJustAddedFocusFistParam() {
+          function ifJustAddedFocusFirstParam() {
             if ($scope.func.added) {
             if ($scope.func.added) {
               $scope.func.added = false;
               $scope.func.added = false;
               setTimeout(function() {
               setTimeout(function() {
@@ -223,7 +272,20 @@ function (angular, _, $) {
               }
               }
 
 
               if ($target.hasClass('fa-question-circle')) {
               if ($target.hasClass('fa-question-circle')) {
-                window.open("http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + funcDef.name,'_blank');
+                var funcDef = ctrl.datasource.getFuncDef(func.def.name);
+                if (funcDef && funcDef.description) {
+                  popoverSrv.show({
+                    element: e.target,
+                    position: 'bottom left',
+                    classNames: 'drop-popover drop-function-def',
+                    template: '<div style="overflow:auto;max-height:30rem;">'
+                      + '<h4>' + funcDef.name + '</h4>' + rst2html(funcDef.description) + '</div>',
+                    openOn: 'click',
+                  });
+                } else {
+                  window.open(
+                    "http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions." + func.def.name,'_blank');
+                }
                 return;
                 return;
               }
               }
             });
             });
@@ -233,7 +295,7 @@ function (angular, _, $) {
             elem.children().remove();
             elem.children().remove();
 
 
             addElementsAndCompile();
             addElementsAndCompile();
-            ifJustAddedFocusFistParam();
+            ifJustAddedFocusFirstParam();
             registerFuncControlsToggle();
             registerFuncControlsToggle();
             registerFuncControlsActions();
             registerFuncControlsActions();
           }
           }

File diff suppressed because it is too large
+ 121 - 217
public/app/plugins/datasource/graphite/gfunc.ts


+ 7 - 6
public/app/plugins/datasource/graphite/graphite_query.ts

@@ -1,8 +1,8 @@
 import _ from 'lodash';
 import _ from 'lodash';
-import gfunc from './gfunc';
 import { Parser } from './parser';
 import { Parser } from './parser';
 
 
 export default class GraphiteQuery {
 export default class GraphiteQuery {
+  datasource: any;
   target: any;
   target: any;
   functions: any[];
   functions: any[];
   segments: any[];
   segments: any[];
@@ -15,7 +15,8 @@ export default class GraphiteQuery {
   scopedVars: any;
   scopedVars: any;
 
 
   /** @ngInject */
   /** @ngInject */
-  constructor(target, templateSrv?, scopedVars?) {
+  constructor(datasource, target, templateSrv?, scopedVars?) {
+    this.datasource = datasource;
     this.target = target;
     this.target = target;
     this.parseTarget();
     this.parseTarget();
 
 
@@ -86,7 +87,7 @@ export default class GraphiteQuery {
 
 
     switch (astNode.type) {
     switch (astNode.type) {
       case 'function':
       case 'function':
-        var innerFunc = gfunc.createFuncInstance(astNode.name, {
+        var innerFunc = this.datasource.createFuncInstance(astNode.name, {
           withDefaultParams: false,
           withDefaultParams: false,
         });
         });
         _.each(astNode.params, param => {
         _.each(astNode.params, param => {
@@ -133,7 +134,7 @@ export default class GraphiteQuery {
 
 
   moveAliasFuncLast() {
   moveAliasFuncLast() {
     var aliasFunc = _.find(this.functions, function(func) {
     var aliasFunc = _.find(this.functions, function(func) {
-      return func.def.name === 'alias' || func.def.name === 'aliasByNode' || func.def.name === 'aliasByMetric';
+      return func.def.name.startsWith('alias');
     });
     });
 
 
     if (aliasFunc) {
     if (aliasFunc) {
@@ -143,7 +144,7 @@ export default class GraphiteQuery {
   }
   }
 
 
   addFunctionParameter(func, value) {
   addFunctionParameter(func, value) {
-    if (func.params.length >= func.def.params.length) {
+    if (func.params.length >= func.def.params.length && !_.get(_.last(func.def.params), 'multiple', false)) {
       throw { message: 'too many parameters for function ' + func.def.name };
       throw { message: 'too many parameters for function ' + func.def.name };
     }
     }
     func.params.push(value);
     func.params.push(value);
@@ -208,7 +209,7 @@ export default class GraphiteQuery {
   }
   }
 
 
   splitSeriesByTagParams(func) {
   splitSeriesByTagParams(func) {
-    const tagPattern = /([^\!=~]+)([\!=~]+)([^\!=~]+)/;
+    const tagPattern = /([^\!=~]+)(\!?=~?)(.*)/;
     return _.flatten(
     return _.flatten(
       _.map(func.params, (param: string) => {
       _.map(func.params, (param: string) => {
         let matches = tagPattern.exec(param);
         let matches = tagPattern.exec(param);

+ 35 - 15
public/app/plugins/datasource/graphite/partials/query.editor.html

@@ -10,30 +10,50 @@
         <label class="gf-form-label width-6 query-keyword">Series</label>
         <label class="gf-form-label width-6 query-keyword">Series</label>
       </div>
       </div>
 
 
-      <div ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
-        <gf-form-dropdown model="tag.key" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-key"
+      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
+        <gf-form-dropdown
+          model="tag.key"
+          lookup-text="false"
+          allow-custom="true"
+          label-mode="true"
+          placeholder="Tag key"
+          css-class="query-segment-key"
           get-options="ctrl.getTags($index, $query)"
           get-options="ctrl.getTags($index, $query)"
-          on-change="ctrl.tagChanged(tag, $index)">
-        </gf-form-dropdown>
-        <gf-form-dropdown model="tag.operator" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-operator"
+          on-change="ctrl.tagChanged(tag, $index)"
+        />
+        <gf-form-dropdown
+          model="tag.operator"
+          lookup-text="false"
+          allow-custom="false"
+          label-mode="true"
+          css-class="query-segment-operator"
           get-options="ctrl.getTagOperators()"
           get-options="ctrl.getTagOperators()"
           on-change="ctrl.tagChanged(tag, $index)"
           on-change="ctrl.tagChanged(tag, $index)"
-					min-input-width="30">
-        </gf-form-dropdown>
-        <gf-form-dropdown model="tag.value" lookup-text="false" allow-custom="false" label-mode="true" css-class="query-segment-value"
+          min-input-width="30"
+        />
+        <gf-form-dropdown
+          model="tag.value"
+          lookup-text="false"
+          allow-custom="true"
+          label-mode="true"
+          css-class="query-segment-value"
+          placeholder="Tag value"
           get-options="ctrl.getTagValues(tag, $index, $query)"
           get-options="ctrl.getTagValues(tag, $index, $query)"
-          on-change="ctrl.tagChanged(tag, $index)">
-        </gf-form-dropdown>
+          on-change="ctrl.tagChanged(tag, $index)"
+        />
         <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
         <label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
       </div>
       </div>
 
 
-      <div ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
-        <metric-segment segment="segment" get-options="ctrl.getAltSegments($index)" on-change="ctrl.segmentValueChanged(segment, $index)"></metric-segment>
+      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" />
+      </div>
+
+      <div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
+        <metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
       </div>
       </div>
 
 
-      <div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
-        <metric-segment segment="segment" get-options="ctrl.getTagsAsSegments()" on-change="ctrl.addNewTag(segment)">
-        </metric-segment>
+      <div ng-if="ctrl.paused" class="gf-form">
+        <a ng-click="ctrl.unpause()" class="gf-form-label query-part"><i class="fa fa-play"></i></a>
       </div>
       </div>
 
 
       <div class="gf-form gf-form--grow">
       <div class="gf-form gf-form--grow">

+ 33 - 20
public/app/plugins/datasource/graphite/query_ctrl.ts

@@ -2,7 +2,6 @@ import './add_graphite_func';
 import './func_editor';
 import './func_editor';
 
 
 import _ from 'lodash';
 import _ from 'lodash';
-import gfunc from './gfunc';
 import GraphiteQuery from './graphite_query';
 import GraphiteQuery from './graphite_query';
 import { QueryCtrl } from 'app/plugins/sdk';
 import { QueryCtrl } from 'app/plugins/sdk';
 import appEvents from 'app/core/app_events';
 import appEvents from 'app/core/app_events';
@@ -18,17 +17,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   addTagSegments: any[];
   addTagSegments: any[];
   removeTagValue: string;
   removeTagValue: string;
   supportsTags: boolean;
   supportsTags: boolean;
+  paused: boolean;
 
 
   /** @ngInject **/
   /** @ngInject **/
-  constructor($scope, $injector, private uiSegmentSrv, private templateSrv) {
+  constructor($scope, $injector, private uiSegmentSrv, private templateSrv, $timeout) {
     super($scope, $injector);
     super($scope, $injector);
     this.supportsTags = this.datasource.supportsTags;
     this.supportsTags = this.datasource.supportsTags;
+    this.paused = false;
+    this.target.target = this.target.target || '';
 
 
-    if (this.target) {
-      this.target.target = this.target.target || '';
-      this.queryModel = new GraphiteQuery(this.target, templateSrv);
+    this.datasource.waitForFuncDefsLoaded().then(() => {
+      this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
       this.buildSegments();
       this.buildSegments();
-    }
+    });
 
 
     this.removeTagValue = '-- remove tag --';
     this.removeTagValue = '-- remove tag --';
   }
   }
@@ -104,8 +105,11 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     });
     });
   }
   }
 
 
-  getAltSegments(index) {
-    var query = index === 0 ? '*' : this.queryModel.getSegmentPathUpTo(index) + '.*';
+  getAltSegments(index, prefix) {
+    var query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
+    if (index > 0) {
+      query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
+    }
     var options = {
     var options = {
       range: this.panelCtrl.range,
       range: this.panelCtrl.range,
       requestId: 'get-alt-segments',
       requestId: 'get-alt-segments',
@@ -121,7 +125,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
           });
           });
         });
         });
 
 
-        if (altSegments.length === 0) {
+        if (index > 0 && altSegments.length === 0) {
           return altSegments;
           return altSegments;
         }
         }
 
 
@@ -158,7 +162,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
         if (this.supportsTags && index === 0) {
         if (this.supportsTags && index === 0) {
           this.removeTaggedEntry(altSegments);
           this.removeTaggedEntry(altSegments);
-          return this.addAltTagSegments(index, altSegments);
+          return this.addAltTagSegments(prefix, altSegments);
         } else {
         } else {
           return altSegments;
           return altSegments;
         }
         }
@@ -168,8 +172,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
       });
       });
   }
   }
 
 
-  addAltTagSegments(index, altSegments) {
-    return this.getTagsAsSegments().then(tagSegments => {
+  addAltTagSegments(prefix, altSegments) {
+    return this.getTagsAsSegments(prefix).then(tagSegments => {
       tagSegments = _.map(tagSegments, segment => {
       tagSegments = _.map(tagSegments, segment => {
         segment.value = TAG_PREFIX + segment.value;
         segment.value = TAG_PREFIX + segment.value;
         return segment;
         return segment;
@@ -192,6 +196,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
     if (segment.type === 'tag') {
     if (segment.type === 'tag') {
       let tag = removeTagPrefix(segment.value);
       let tag = removeTagPrefix(segment.value);
+      this.pause();
       this.addSeriesByTagFunc(tag);
       this.addSeriesByTagFunc(tag);
       return;
       return;
     }
     }
@@ -236,13 +241,13 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     var oldTarget = this.queryModel.target.target;
     var oldTarget = this.queryModel.target.target;
     this.updateModelTarget();
     this.updateModelTarget();
 
 
-    if (this.queryModel.target !== oldTarget) {
+    if (this.queryModel.target !== oldTarget && !this.paused) {
       this.panelCtrl.refresh();
       this.panelCtrl.refresh();
     }
     }
   }
   }
 
 
   addFunction(funcDef) {
   addFunction(funcDef) {
-    var newFunc = gfunc.createFuncInstance(funcDef, {
+    var newFunc = this.datasource.createFuncInstance(funcDef, {
       withDefaultParams: true,
       withDefaultParams: true,
     });
     });
     newFunc.added = true;
     newFunc.added = true;
@@ -268,11 +273,10 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   }
   }
 
 
   addSeriesByTagFunc(tag) {
   addSeriesByTagFunc(tag) {
-    let funcDef = gfunc.getFuncDef('seriesByTag');
-    let newFunc = gfunc.createFuncInstance(funcDef, {
+    let newFunc = this.datasource.createFuncInstance('seriesByTag', {
       withDefaultParams: false,
       withDefaultParams: false,
     });
     });
-    let tagParam = `${tag}=select tag value`;
+    let tagParam = `${tag}=`;
     newFunc.params = [tagParam];
     newFunc.params = [tagParam];
     this.queryModel.addFunction(newFunc);
     this.queryModel.addFunction(newFunc);
     newFunc.added = true;
     newFunc.added = true;
@@ -314,9 +318,9 @@ export class GraphiteQueryCtrl extends QueryCtrl {
     });
     });
   }
   }
 
 
-  getTagsAsSegments() {
+  getTagsAsSegments(tagPrefix) {
     let tagExpressions = this.queryModel.renderTagExpressions();
     let tagExpressions = this.queryModel.renderTagExpressions();
-    return this.datasource.getTagsAutoComplete(tagExpressions).then(values => {
+    return this.datasource.getTagsAutoComplete(tagExpressions, tagPrefix).then(values => {
       return _.map(values, val => {
       return _.map(values, val => {
         return this.uiSegmentSrv.newSegment({
         return this.uiSegmentSrv.newSegment({
           value: val.text,
           value: val.text,
@@ -355,7 +359,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
 
 
   addNewTag(segment) {
   addNewTag(segment) {
     let newTagKey = segment.value;
     let newTagKey = segment.value;
-    let newTag = { key: newTagKey, operator: '=', value: 'select tag value' };
+    let newTag = { key: newTagKey, operator: '=', value: '' };
     this.queryModel.addTag(newTag);
     this.queryModel.addTag(newTag);
     this.targetChanged();
     this.targetChanged();
     this.fixTagSegments();
     this.fixTagSegments();
@@ -374,6 +378,15 @@ export class GraphiteQueryCtrl extends QueryCtrl {
   showDelimiter(index) {
   showDelimiter(index) {
     return index !== this.queryModel.tags.length - 1;
     return index !== this.queryModel.tags.length - 1;
   }
   }
+
+  pause() {
+    this.paused = true;
+  }
+
+  unpause() {
+    this.paused = false;
+    this.panelCtrl.refresh();
+  }
 }
 }
 
 
 function mapToDropdownOptions(results) {
 function mapToDropdownOptions(results) {

+ 6 - 5
public/app/plugins/datasource/graphite/specs/gfunc.jest.ts

@@ -5,7 +5,8 @@ describe('when creating func instance from func names', function() {
     var func = gfunc.createFuncInstance('sumSeries');
     var func = gfunc.createFuncInstance('sumSeries');
     expect(func).toBeTruthy();
     expect(func).toBeTruthy();
     expect(func.def.name).toEqual('sumSeries');
     expect(func.def.name).toEqual('sumSeries');
-    expect(func.def.params.length).toEqual(5);
+    expect(func.def.params.length).toEqual(1);
+    expect(func.def.params[0].multiple).toEqual(true);
     expect(func.def.defaultParams.length).toEqual(1);
     expect(func.def.defaultParams.length).toEqual(1);
   });
   });
 
 
@@ -74,10 +75,10 @@ describe('when rendering func instance', function() {
   });
   });
 });
 });
 
 
-describe('when requesting function categories', function() {
-  it('should return function categories', function() {
-    var catIndex = gfunc.getCategories('1.0');
-    expect(catIndex.Special.length).toBeGreaterThan(8);
+describe('when requesting function definitions', function() {
+  it('should return function definitions', function() {
+    var funcIndex = gfunc.getFuncDefs('1.0');
+    expect(Object.keys(funcIndex).length).toBeGreaterThan(8);
   });
   });
 });
 });
 
 

+ 20 - 6
public/app/plugins/datasource/graphite/specs/query_ctrl_specs.ts

@@ -24,6 +24,10 @@ describe('GraphiteQueryCtrl', function() {
       ctx.scope = $rootScope.$new();
       ctx.scope = $rootScope.$new();
       ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
       ctx.target = { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' };
       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
       ctx.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([]));
+      ctx.datasource.getFuncDefs = sinon.stub().returns(ctx.$q.when(gfunc.getFuncDefs('1.0')));
+      ctx.datasource.getFuncDef = gfunc.getFuncDef;
+      ctx.datasource.waitForFuncDefsLoaded = sinon.stub().returns(ctx.$q.when(null));
+      ctx.datasource.createFuncInstance = gfunc.createFuncInstance;
       ctx.panelCtrl = { panel: {} };
       ctx.panelCtrl = { panel: {} };
       ctx.panelCtrl = {
       ctx.panelCtrl = {
         panel: {
         panel: {
@@ -180,7 +184,21 @@ describe('GraphiteQueryCtrl', function() {
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
       ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
       ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
       ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{ expandable: false }]));
       ctx.ctrl.parseTarget();
       ctx.ctrl.parseTarget();
+    });
+
+    it('should add function params', function() {
+      expect(ctx.ctrl.queryModel.segments.length).to.be(1);
+      expect(ctx.ctrl.queryModel.segments[0].value).to.be('#A');
+
+      expect(ctx.ctrl.queryModel.functions[0].params.length).to.be(1);
+      expect(ctx.ctrl.queryModel.functions[0].params[0]).to.be(60);
+    });
+
+    it('target should remain the same', function() {
+      expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
+    });
 
 
+    it('targetFull should include nested queries', function() {
       ctx.ctrl.panelCtrl.panel.targets = [
       ctx.ctrl.panelCtrl.panel.targets = [
         {
         {
           target: 'nested.query.count',
           target: 'nested.query.count',
@@ -189,13 +207,9 @@ describe('GraphiteQueryCtrl', function() {
       ];
       ];
 
 
       ctx.ctrl.updateModelTarget();
       ctx.ctrl.updateModelTarget();
-    });
 
 
-    it('target should remain the same', function() {
       expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
       expect(ctx.ctrl.target.target).to.be('scaleToSeconds(#A, 60)');
-    });
 
 
-    it('targetFull should include nexted queries', function() {
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
       expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count, 60)');
     });
     });
   });
   });
@@ -271,12 +285,12 @@ describe('GraphiteQueryCtrl', function() {
     });
     });
 
 
     it('should update tags with default value', function() {
     it('should update tags with default value', function() {
-      const expected = [{ key: 'tag1', operator: '=', value: 'select tag value' }];
+      const expected = [{ key: 'tag1', operator: '=', value: '' }];
       expect(ctx.ctrl.queryModel.tags).to.eql(expected);
       expect(ctx.ctrl.queryModel.tags).to.eql(expected);
     });
     });
 
 
     it('should update target', function() {
     it('should update target', function() {
-      const expected = "seriesByTag('tag1=select tag value')";
+      const expected = "seriesByTag('tag1=')";
       expect(ctx.ctrl.target.target).to.eql(expected);
       expect(ctx.ctrl.target.target).to.eql(expected);
     });
     });
   });
   });

+ 1 - 1
public/app/plugins/datasource/influxdb/partials/annotations.editor.html

@@ -1,7 +1,7 @@
 
 
 <div class="gf-form-group">
 <div class="gf-form-group">
 	<div class="gf-form">
 	<div class="gf-form">
-		<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter"></input>
+		<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter limit 1000"></input>
 	</div>
 	</div>
 </div>
 </div>
 
 

+ 1 - 1
public/app/plugins/datasource/mysql/response_parser.ts

@@ -138,7 +138,7 @@ export default class ResponseParser {
       list.push({
       list.push({
         annotation: options.annotation,
         annotation: options.annotation,
         time: Math.floor(row[timeColumnIndex]) * 1000,
         time: Math.floor(row[timeColumnIndex]) * 1000,
-        text: row[textColumnIndex],
+        text: row[textColumnIndex] ? row[textColumnIndex].toString() : '',
         tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [],
         tags: row[tagsColumnIndex] ? row[tagsColumnIndex].trim().split(/\s*,\s*/) : [],
       });
       });
     }
     }

+ 250 - 74
public/app/plugins/datasource/prometheus/completer.ts

@@ -17,66 +17,22 @@ export class PromCompleter {
   getCompletions(editor, session, pos, prefix, callback) {
   getCompletions(editor, session, pos, prefix, callback) {
     let token = session.getTokenAt(pos.row, pos.column);
     let token = session.getTokenAt(pos.row, pos.column);
 
 
-    var metricName;
     switch (token.type) {
     switch (token.type) {
-      case 'entity.name.tag':
-        metricName = this.findMetricName(session, pos.row, pos.column);
-        if (!metricName) {
-          callback(null, this.transformToCompletions(['__name__', 'instance', 'job'], 'label name'));
-          return;
-        }
-
-        if (this.labelNameCache[metricName]) {
-          callback(null, this.labelNameCache[metricName]);
-          return;
-        }
-
-        return this.getLabelNameAndValueForMetric(metricName).then(result => {
-          var labelNames = this.transformToCompletions(
-            _.uniq(
-              _.flatten(
-                result.map(r => {
-                  return Object.keys(r.metric);
-                })
-              )
-            ),
-            'label name'
-          );
-          this.labelNameCache[metricName] = labelNames;
-          callback(null, labelNames);
+      case 'entity.name.tag.label-matcher':
+        this.getCompletionsForLabelMatcherName(session, pos).then(completions => {
+          callback(null, completions);
         });
         });
-      case 'string.quoted':
-        metricName = this.findMetricName(session, pos.row, pos.column);
-        if (!metricName) {
-          callback(null, []);
-          return;
-        }
-
-        var labelNameToken = this.findToken(session, pos.row, pos.column, 'entity.name.tag', null, 'paren.lparen');
-        if (!labelNameToken) {
-          callback(null, []);
-          return;
-        }
-        var labelName = labelNameToken.value;
-
-        if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) {
-          callback(null, this.labelValueCache[metricName][labelName]);
-          return;
-        }
-
-        return this.getLabelNameAndValueForMetric(metricName).then(result => {
-          var labelValues = this.transformToCompletions(
-            _.uniq(
-              result.map(r => {
-                return r.metric[labelName];
-              })
-            ),
-            'label value'
-          );
-          this.labelValueCache[metricName] = this.labelValueCache[metricName] || {};
-          this.labelValueCache[metricName][labelName] = labelValues;
-          callback(null, labelValues);
+        return;
+      case 'string.quoted.label-matcher':
+        this.getCompletionsForLabelMatcherValue(session, pos).then(completions => {
+          callback(null, completions);
         });
         });
+        return;
+      case 'entity.name.tag.label-list-matcher':
+        this.getCompletionsForBinaryOperator(session, pos).then(completions => {
+          callback(null, completions);
+        });
+        return;
     }
     }
 
 
     if (token.type === 'paren.lparen' && token.value === '[') {
     if (token.type === 'paren.lparen' && token.value === '[') {
@@ -128,17 +84,186 @@ export class PromCompleter {
     });
     });
   }
   }
 
 
-  getLabelNameAndValueForMetric(metricName) {
-    if (this.labelQueryCache[metricName]) {
-      return Promise.resolve(this.labelQueryCache[metricName]);
+  getCompletionsForLabelMatcherName(session, pos) {
+    let metricName = this.findMetricName(session, pos.row, pos.column);
+    if (!metricName) {
+      return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name'));
+    }
+
+    if (this.labelNameCache[metricName]) {
+      return Promise.resolve(this.labelNameCache[metricName]);
+    }
+
+    return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
+      var labelNames = this.transformToCompletions(
+        _.uniq(
+          _.flatten(
+            result.map(r => {
+              return Object.keys(r.metric);
+            })
+          )
+        ),
+        'label name'
+      );
+      this.labelNameCache[metricName] = labelNames;
+      return Promise.resolve(labelNames);
+    });
+  }
+
+  getCompletionsForLabelMatcherValue(session, pos) {
+    let metricName = this.findMetricName(session, pos.row, pos.column);
+    if (!metricName) {
+      return Promise.resolve([]);
+    }
+
+    var labelNameToken = this.findToken(
+      session,
+      pos.row,
+      pos.column,
+      'entity.name.tag.label-matcher',
+      null,
+      'paren.lparen.label-matcher'
+    );
+    if (!labelNameToken) {
+      return Promise.resolve([]);
+    }
+    var labelName = labelNameToken.value;
+
+    if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) {
+      return Promise.resolve(this.labelValueCache[metricName][labelName]);
+    }
+
+    return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
+      var labelValues = this.transformToCompletions(
+        _.uniq(
+          result.map(r => {
+            return r.metric[labelName];
+          })
+        ),
+        'label value'
+      );
+      this.labelValueCache[metricName] = this.labelValueCache[metricName] || {};
+      this.labelValueCache[metricName][labelName] = labelValues;
+      return Promise.resolve(labelValues);
+    });
+  }
+
+  getCompletionsForBinaryOperator(session, pos) {
+    let keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier');
+    if (!keywordOperatorToken) {
+      return Promise.resolve([]);
+    }
+    let rparenToken, expr;
+    switch (keywordOperatorToken.value) {
+      case 'by':
+      case 'without':
+        rparenToken = this.findToken(
+          session,
+          keywordOperatorToken.row,
+          keywordOperatorToken.column,
+          'paren.rparen',
+          null,
+          'identifier'
+        );
+        if (!rparenToken) {
+          return Promise.resolve([]);
+        }
+        expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
+        if (expr === '') {
+          return Promise.resolve([]);
+        }
+        return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
+          var labelNames = this.transformToCompletions(
+            _.uniq(
+              _.flatten(
+                result.map(r => {
+                  return Object.keys(r.metric);
+                })
+              )
+            ),
+            'label name'
+          );
+          this.labelNameCache[expr] = labelNames;
+          return labelNames;
+        });
+      case 'on':
+      case 'ignoring':
+      case 'group_left':
+      case 'group_right':
+        let binaryOperatorToken = this.findToken(
+          session,
+          keywordOperatorToken.row,
+          keywordOperatorToken.column,
+          'keyword.operator.binary',
+          null,
+          'identifier'
+        );
+        if (!binaryOperatorToken) {
+          return Promise.resolve([]);
+        }
+        rparenToken = this.findToken(
+          session,
+          binaryOperatorToken.row,
+          binaryOperatorToken.column,
+          'paren.rparen',
+          null,
+          'identifier'
+        );
+        if (rparenToken) {
+          expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column);
+          if (expr === '') {
+            return Promise.resolve([]);
+          }
+          return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => {
+            var labelNames = this.transformToCompletions(
+              _.uniq(
+                _.flatten(
+                  result.map(r => {
+                    return Object.keys(r.metric);
+                  })
+                )
+              ),
+              'label name'
+            );
+            this.labelNameCache[expr] = labelNames;
+            return labelNames;
+          });
+        } else {
+          let metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column);
+          return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => {
+            var labelNames = this.transformToCompletions(
+              _.uniq(
+                _.flatten(
+                  result.map(r => {
+                    return Object.keys(r.metric);
+                  })
+                )
+              ),
+              'label name'
+            );
+            this.labelNameCache[metricName] = labelNames;
+            return Promise.resolve(labelNames);
+          });
+        }
+    }
+
+    return Promise.resolve([]);
+  }
+
+  getLabelNameAndValueForExpression(expr, type) {
+    if (this.labelQueryCache[expr]) {
+      return Promise.resolve(this.labelQueryCache[expr]);
     }
     }
-    var op = '=~';
-    if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(metricName)) {
-      op = '=';
+    let query = expr;
+    if (type === 'metricName') {
+      let op = '=~';
+      if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(expr)) {
+        op = '=';
+      }
+      query = '{__name__' + op + '"' + expr + '"}';
     }
     }
-    var expr = '{__name__' + op + '"' + metricName + '"}';
-    return this.datasource.performInstantQuery({ expr: expr }, new Date().getTime() / 1000).then(response => {
-      this.labelQueryCache[metricName] = response.data.data.result;
+    return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => {
+      this.labelQueryCache[expr] = response.data.data.result;
       return response.data.data.result;
       return response.data.data.result;
     });
     });
   }
   }
@@ -158,20 +283,25 @@ export class PromCompleter {
     var metricName = '';
     var metricName = '';
 
 
     var tokens;
     var tokens;
-    var nameLabelNameToken = this.findToken(session, row, column, 'entity.name.tag', '__name__', 'paren.lparen');
+    var nameLabelNameToken = this.findToken(
+      session,
+      row,
+      column,
+      'entity.name.tag.label-matcher',
+      '__name__',
+      'paren.lparen.label-matcher'
+    );
     if (nameLabelNameToken) {
     if (nameLabelNameToken) {
       tokens = session.getTokens(nameLabelNameToken.row);
       tokens = session.getTokens(nameLabelNameToken.row);
       var nameLabelValueToken = tokens[nameLabelNameToken.index + 2];
       var nameLabelValueToken = tokens[nameLabelNameToken.index + 2];
-      if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted') {
+      if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted.label-matcher') {
         metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation
         metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation
       }
       }
     } else {
     } else {
       var metricNameToken = this.findToken(session, row, column, 'identifier', null, null);
       var metricNameToken = this.findToken(session, row, column, 'identifier', null, null);
       if (metricNameToken) {
       if (metricNameToken) {
         tokens = session.getTokens(metricNameToken.row);
         tokens = session.getTokens(metricNameToken.row);
-        if (tokens[metricNameToken.index + 1].type === 'paren.lparen') {
-          metricName = metricNameToken.value;
-        }
+        metricName = metricNameToken.value;
       }
       }
     }
     }
 
 
@@ -180,19 +310,28 @@ export class PromCompleter {
 
 
   findToken(session, row, column, target, value, guard) {
   findToken(session, row, column, target, value, guard) {
     var tokens, idx;
     var tokens, idx;
+    // find index and get column of previous token
     for (var r = row; r >= 0; r--) {
     for (var r = row; r >= 0; r--) {
+      let c;
       tokens = session.getTokens(r);
       tokens = session.getTokens(r);
       if (r === row) {
       if (r === row) {
         // current row
         // current row
-        var c = 0;
+        c = 0;
         for (idx = 0; idx < tokens.length; idx++) {
         for (idx = 0; idx < tokens.length; idx++) {
-          c += tokens[idx].value.length;
-          if (c >= column) {
+          let nc = c + tokens[idx].value.length;
+          if (nc >= column) {
             break;
             break;
           }
           }
+          c = nc;
         }
         }
       } else {
       } else {
         idx = tokens.length - 1;
         idx = tokens.length - 1;
+        c =
+          _.sum(
+            tokens.map(t => {
+              return t.value.length;
+            })
+          ) - tokens[tokens.length - 1].value.length;
       }
       }
 
 
       for (; idx >= 0; idx--) {
       for (; idx >= 0; idx--) {
@@ -202,12 +341,49 @@ export class PromCompleter {
 
 
         if (tokens[idx].type === target && (!value || tokens[idx].value === value)) {
         if (tokens[idx].type === target && (!value || tokens[idx].value === value)) {
           tokens[idx].row = r;
           tokens[idx].row = r;
+          tokens[idx].column = c;
           tokens[idx].index = idx;
           tokens[idx].index = idx;
           return tokens[idx];
           return tokens[idx];
         }
         }
+        c -= tokens[idx].value.length;
       }
       }
     }
     }
 
 
     return null;
     return null;
   }
   }
+
+  findExpressionMatchedParen(session, row, column) {
+    let tokens, idx;
+    let deep = 1;
+    let expression = ')';
+    for (let r = row; r >= 0; r--) {
+      tokens = session.getTokens(r);
+      if (r === row) {
+        // current row
+        let c = 0;
+        for (idx = 0; idx < tokens.length; idx++) {
+          c += tokens[idx].value.length;
+          if (c >= column) {
+            break;
+          }
+        }
+      } else {
+        idx = tokens.length - 1;
+      }
+
+      for (; idx >= 0; idx--) {
+        expression = tokens[idx].value + expression;
+        if (tokens[idx].type === 'paren.rparen') {
+          deep++;
+        } else if (tokens[idx].type === 'paren.lparen') {
+          deep--;
+          if (deep === 0) {
+            return expression;
+          }
+        }
+      }
+    }
+
+    return expression;
+  }
 }
 }

+ 34 - 12
public/app/plugins/datasource/prometheus/mode-prometheus.js

@@ -8,7 +8,6 @@ var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules;
 
 
 var PrometheusHighlightRules = function() {
 var PrometheusHighlightRules = function() {
   var keywords = (
   var keywords = (
-    "by|without|keep_common|offset|bool|and|or|unless|ignoring|on|group_left|group_right|" +
     "count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile"
     "count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile"
   );
   );
 
 
@@ -41,45 +40,66 @@ var PrometheusHighlightRules = function() {
     }, {
     }, {
       token : "constant.language", // time
       token : "constant.language", // time
       regex : "\\d+[smhdwy]"
       regex : "\\d+[smhdwy]"
+    }, {
+      token : "keyword.operator.binary",
+      regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>|and|or|unless"
+    }, {
+      token : "keyword.other",
+      regex : "keep_common|offset|bool"
+    }, {
+      token : "keyword.control",
+      regex : "by|without|on|ignoring|group_left|group_right",
+      next  : "start-label-list-matcher"
     }, {
     }, {
       token : keywordMapper,
       token : keywordMapper,
       regex : "[a-zA-Z_:][a-zA-Z0-9_:]*"
       regex : "[a-zA-Z_:][a-zA-Z0-9_:]*"
-    }, {
-      token : "keyword.operator",
-      regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>"
     }, {
     }, {
       token : "paren.lparen",
       token : "paren.lparen",
       regex : "[[(]"
       regex : "[[(]"
     }, {
     }, {
-      token : "paren.lparen",
+      token : "paren.lparen.label-matcher",
       regex : "{",
       regex : "{",
       next  : "start-label-matcher"
       next  : "start-label-matcher"
     }, {
     }, {
       token : "paren.rparen",
       token : "paren.rparen",
       regex : "[\\])]"
       regex : "[\\])]"
     }, {
     }, {
-      token : "paren.rparen",
+      token : "paren.rparen.label-matcher",
       regex : "}"
       regex : "}"
     }, {
     }, {
       token : "text",
       token : "text",
       regex : "\\s+"
       regex : "\\s+"
     } ],
     } ],
     "start-label-matcher" : [ {
     "start-label-matcher" : [ {
-      token : "entity.name.tag",
+      token : "entity.name.tag.label-matcher",
       regex : '[a-zA-Z_][a-zA-Z0-9_]*'
       regex : '[a-zA-Z_][a-zA-Z0-9_]*'
     }, {
     }, {
-      token : "keyword.operator",
+      token : "keyword.operator.label-matcher",
       regex : '=~|=|!~|!='
       regex : '=~|=|!~|!='
     }, {
     }, {
-      token : "string.quoted",
+      token : "string.quoted.label-matcher",
       regex : '"[^"]*"|\'[^\']*\''
       regex : '"[^"]*"|\'[^\']*\''
     }, {
     }, {
-      token : "punctuation.operator",
+      token : "punctuation.operator.label-matcher",
       regex : ","
       regex : ","
     }, {
     }, {
-      token : "paren.rparen",
+      token : "paren.rparen.label-matcher",
       regex : "}",
       regex : "}",
       next  : "start"
       next  : "start"
+    } ],
+    "start-label-list-matcher" : [ {
+      token : "paren.lparen.label-list-matcher",
+      regex : "[(]"
+    }, {
+      token : "entity.name.tag.label-list-matcher",
+      regex : '[a-zA-Z_][a-zA-Z0-9_]*'
+    }, {
+      token : "punctuation.operator.label-list-matcher",
+      regex : ","
+    }, {
+      token : "paren.rparen.label-list-matcher",
+      regex : "[)]",
+      next  : "start"
     } ]
     } ]
   };
   };
 
 
@@ -395,7 +415,9 @@ var PrometheusCompletions = function() {};
 (function() {
 (function() {
   this.getCompletions = function(state, session, pos, prefix, callback) {
   this.getCompletions = function(state, session, pos, prefix, callback) {
     var token = session.getTokenAt(pos.row, pos.column);
     var token = session.getTokenAt(pos.row, pos.column);
-    if (token.type === 'entity.name.tag' || token.type === 'string.quoted') {
+    if (token.type === 'entity.name.tag.label-matcher'
+      || token.type === 'string.quoted.label-matcher'
+      || token.type === 'entity.name.tag.label-list-matcher') {
       return callback(null, []);
       return callback(null, []);
     }
     }
 
 

+ 69 - 18
public/app/plugins/datasource/prometheus/specs/completer_specs.ts

@@ -61,16 +61,21 @@ describe('Prometheus editor completer', function() {
     it('Should return label name list', () => {
     it('Should return label name list', () => {
       const session = getSessionStub({
       const session = getSessionStub({
         currentToken: {
         currentToken: {
-          type: 'entity.name.tag',
+          type: 'entity.name.tag.label-matcher',
           value: 'j',
           value: 'j',
           index: 2,
           index: 2,
           start: 9,
           start: 9,
         },
         },
         tokens: [
         tokens: [
           { type: 'identifier', value: 'node_cpu' },
           { type: 'identifier', value: 'node_cpu' },
-          { type: 'paren.lparen', value: '{' },
-          { type: 'entity.name.tag', value: 'j', index: 2, start: 9 },
-          { type: 'paren.rparen', value: '}' },
+          { type: 'paren.lparen.label-matcher', value: '{' },
+          {
+            type: 'entity.name.tag.label-matcher',
+            value: 'j',
+            index: 2,
+            start: 9,
+          },
+          { type: 'paren.rparen.label-matcher', value: '}' },
         ],
         ],
         line: 'node_cpu{j}',
         line: 'node_cpu{j}',
       });
       });
@@ -85,19 +90,24 @@ describe('Prometheus editor completer', function() {
     it('Should return label name list', () => {
     it('Should return label name list', () => {
       const session = getSessionStub({
       const session = getSessionStub({
         currentToken: {
         currentToken: {
-          type: 'entity.name.tag',
+          type: 'entity.name.tag.label-matcher',
           value: 'j',
           value: 'j',
           index: 5,
           index: 5,
           start: 22,
           start: 22,
         },
         },
         tokens: [
         tokens: [
-          { type: 'paren.lparen', value: '{' },
-          { type: 'entity.name.tag', value: '__name__' },
-          { type: 'keyword.operator', value: '=~' },
-          { type: 'string.quoted', value: '"node_cpu"' },
-          { type: 'punctuation.operator', value: ',' },
-          { type: 'entity.name.tag', value: 'j', index: 5, start: 22 },
-          { type: 'paren.rparen', value: '}' },
+          { type: 'paren.lparen.label-matcher', value: '{' },
+          { type: 'entity.name.tag.label-matcher', value: '__name__' },
+          { type: 'keyword.operator.label-matcher', value: '=~' },
+          { type: 'string.quoted.label-matcher', value: '"node_cpu"' },
+          { type: 'punctuation.operator.label-matcher', value: ',' },
+          {
+            type: 'entity.name.tag.label-matcher',
+            value: 'j',
+            index: 5,
+            start: 22,
+          },
+          { type: 'paren.rparen.label-matcher', value: '}' },
         ],
         ],
         line: '{__name__=~"node_cpu",j}',
         line: '{__name__=~"node_cpu",j}',
       });
       });
@@ -112,18 +122,23 @@ describe('Prometheus editor completer', function() {
     it('Should return label value list', () => {
     it('Should return label value list', () => {
       const session = getSessionStub({
       const session = getSessionStub({
         currentToken: {
         currentToken: {
-          type: 'string.quoted',
+          type: 'string.quoted.label-matcher',
           value: '"n"',
           value: '"n"',
           index: 4,
           index: 4,
           start: 13,
           start: 13,
         },
         },
         tokens: [
         tokens: [
           { type: 'identifier', value: 'node_cpu' },
           { type: 'identifier', value: 'node_cpu' },
-          { type: 'paren.lparen', value: '{' },
-          { type: 'entity.name.tag', value: 'job' },
-          { type: 'keyword.operator', value: '=' },
-          { type: 'string.quoted', value: '"n"', index: 4, start: 13 },
-          { type: 'paren.rparen', value: '}' },
+          { type: 'paren.lparen.label-matcher', value: '{' },
+          { type: 'entity.name.tag.label-matcher', value: 'job' },
+          { type: 'keyword.operator.label-matcher', value: '=' },
+          {
+            type: 'string.quoted.label-matcher',
+            value: '"n"',
+            index: 4,
+            start: 13,
+          },
+          { type: 'paren.rparen.label-matcher', value: '}' },
         ],
         ],
         line: 'node_cpu{job="n"}',
         line: 'node_cpu{job="n"}',
       });
       });
@@ -133,4 +148,40 @@ describe('Prometheus editor completer', function() {
       });
       });
     });
     });
   });
   });
+
+  describe('When inside by', () => {
+    it('Should return label name list', () => {
+      const session = getSessionStub({
+        currentToken: {
+          type: 'entity.name.tag.label-list-matcher',
+          value: 'm',
+          index: 9,
+          start: 22,
+        },
+        tokens: [
+          { type: 'paren.lparen', value: '(' },
+          { type: 'keyword', value: 'count' },
+          { type: 'paren.lparen', value: '(' },
+          { type: 'identifier', value: 'node_cpu' },
+          { type: 'paren.rparen', value: '))' },
+          { type: 'text', value: ' ' },
+          { type: 'keyword.control', value: 'by' },
+          { type: 'text', value: ' ' },
+          { type: 'paren.lparen.label-list-matcher', value: '(' },
+          {
+            type: 'entity.name.tag.label-list-matcher',
+            value: 'm',
+            index: 9,
+            start: 22,
+          },
+          { type: 'paren.rparen.label-list-matcher', value: ')' },
+        ],
+        line: '(count(node_cpu)) by (m)',
+      });
+
+      return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'm', (s, res) => {
+        expect(res[0].meta).to.eql('label name');
+      });
+    });
+  });
 });
 });

+ 7 - 7
public/app/plugins/panel/graph/tab_display.html

@@ -26,24 +26,24 @@
 		</div>
 		</div>
 		<div class="section gf-form-group">
 		<div class="section gf-form-group">
 			<h5 class="section-heading">Mode Options</h5>
 			<h5 class="section-heading">Mode Options</h5>
-			<div class="gf-form" ng-show="ctrl.panel.lines">
+			<div class="gf-form">
 				<label class="gf-form-label width-8">Fill</label>
 				<label class="gf-form-label width-8">Fill</label>
 				<div class="gf-form-select-wrapper max-width-5">
 				<div class="gf-form-select-wrapper max-width-5">
-					<select class="gf-form-input" ng-model="ctrl.panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
+					<select class="gf-form-input" ng-model="ctrl.panel.fill" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()" ng-disabled="!ctrl.panel.lines"></select>
 				</div>
 				</div>
 			</div>
 			</div>
-			<div class="gf-form" ng-show="(ctrl.panel.lines)">
+			<div class="gf-form">
 				<label class="gf-form-label width-8">Line Width</label>
 				<label class="gf-form-label width-8">Line Width</label>
 				<div class="gf-form-select-wrapper max-width-5">
 				<div class="gf-form-select-wrapper max-width-5">
-					<select class="gf-form-input" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
+					<select class="gf-form-input" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()" ng-disabled="!ctrl.panel.lines"></select>
 				</div>
 				</div>
 			</div>
 			</div>
-			<gf-form-switch ng-show="ctrl.panel.lines" class="gf-form" label="Staircase" label-class="width-8" checked="ctrl.panel.steppedLine" on-change="ctrl.render()">
+			<gf-form-switch ng-disabled="!ctrl.panel.lines" class="gf-form" label="Staircase" label-class="width-8" checked="ctrl.panel.steppedLine" on-change="ctrl.render()">
 			</gf-form-switch>
 			</gf-form-switch>
-			<div class="gf-form" ng-show="ctrl.panel.points">
+			<div class="gf-form">
 				<label class="gf-form-label width-8">Point Radius</label>
 				<label class="gf-form-label width-8">Point Radius</label>
 				<div class="gf-form-select-wrapper max-width-5">
 				<div class="gf-form-select-wrapper max-width-5">
-					<select class="gf-form-input" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
+					<select class="gf-form-input" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()" ng-disabled="!ctrl.panel.points"></select>
 				</div>
 				</div>
 			</div>
 			</div>
 		</div>
 		</div>

+ 2 - 2
public/app/plugins/panel/singlestat/editor.html

@@ -29,7 +29,7 @@
         <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
         <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.prefix" ng-change="ctrl.render()" ng-model-onblur>
         <label class="gf-form-label width-6">Font size</label>
         <label class="gf-form-label width-6">Font size</label>
         <div class="gf-form-select-wrapper">
         <div class="gf-form-select-wrapper">
-          <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
+          <select class="gf-form-input" ng-model="ctrl.panel.prefixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select>
         </div>
         </div>
       </div>
       </div>
     </div>
     </div>
@@ -39,7 +39,7 @@
       <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
       <input type="text" class="gf-form-input width-12" ng-model="ctrl.panel.postfix" ng-change="ctrl.render()" ng-model-onblur>
       <label class="gf-form-label width-6">Font size</label>
       <label class="gf-form-label width-6">Font size</label>
       <div class="gf-form-select-wrapper">
       <div class="gf-form-select-wrapper">
-        <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()"></select>
+        <select class="input-small gf-form-input" ng-model="ctrl.panel.postfixFontSize" ng-options="f for f in ctrl.fontSizes" ng-change="ctrl.render()" ng-disabled="ctrl.canChangeFontSize()"></select>
       </div>
       </div>
     </div>
     </div>
     <div class="gf-form">
     <div class="gf-form">

+ 4 - 0
public/app/plugins/panel/singlestat/module.ts

@@ -198,6 +198,10 @@ class SingleStatCtrl extends MetricsPanelCtrl {
     this.setValueMapping(data);
     this.setValueMapping(data);
   }
   }
 
 
+  canChangeFontSize() {
+    return this.panel.gauge.show;
+  }
+
   setColoring(options) {
   setColoring(options) {
     if (options.background) {
     if (options.background) {
       this.panel.colorValue = false;
       this.panel.colorValue = false;

+ 26 - 0
public/app/stores/ViewStore/ViewStore.jest.ts

@@ -0,0 +1,26 @@
+import { ViewStore } from './ViewStore';
+import { toJS } from 'mobx';
+
+describe('ViewStore', () => {
+  let store;
+
+  beforeAll(() => {
+    store = ViewStore.create({
+      path: '',
+      query: {},
+    });
+  });
+
+  it('Can update path and query', () => {
+    store.updatePathAndQuery('/hello', { key: 1, otherParam: 'asd' });
+    expect(store.path).toBe('/hello');
+    expect(store.query.get('key')).toBe(1);
+    expect(store.currentUrl).toBe('/hello?key=1&otherParam=asd');
+  });
+
+  it('Query can contain arrays', () => {
+    store.updatePathAndQuery('/hello', { values: ['A', 'B'] });
+    expect(toJS(store.query.get('values'))).toMatchObject(['A', 'B']);
+    expect(store.currentUrl).toBe('/hello?values=A&values=B');
+  });
+});

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