Pārlūkot izejas kodu

Merge branch 'master' into postgres-query-builder

Sven Klemm 7 gadi atpakaļ
vecāks
revīzija
15c8ef6c95
54 mainītis faili ar 2167 papildinājumiem un 2595 dzēšanām
  1. 1 1
      .bra.toml
  2. 1 0
      .circleci/config.yml
  3. 0 13
      .jscs.json
  4. 0 37
      .jshintrc
  5. 11 2
      CHANGELOG.md
  6. 0 1
      Gruntfile.js
  7. 3 0
      conf/defaults.ini
  8. 3 0
      conf/ldap.toml
  9. 4 0
      conf/sample.ini
  10. 2 0
      docs/sources/features/datasources/postgres.md
  11. 33 0
      docs/sources/http_api/user.md
  12. 3 0
      docs/sources/installation/ldap.md
  13. 0 3
      package.json
  14. 1 0
      pkg/api/api.go
  15. 15 0
      pkg/api/user.go
  16. 10 0
      pkg/login/ldap.go
  17. 2 0
      pkg/login/ldap_settings.go
  18. 1 2
      pkg/services/alerting/notifier.go
  19. 11 5
      pkg/tsdb/postgres/macros.go
  20. 20 1
      pkg/tsdb/postgres/macros_test.go
  21. 3 1
      pkg/tsdb/postgres/postgres.go
  22. 1 1
      pkg/tsdb/postgres/postgres_test.go
  23. 3 2
      public/app/containers/Teams/TeamMembers.tsx
  24. 1 0
      public/app/core/components/help/help.ts
  25. 3 12
      public/app/core/services/keybindingSrv.ts
  26. 111 113
      public/app/features/dashboard/shareModalCtrl.ts
  27. 150 0
      public/app/features/dashboard/specs/share_modal_ctrl.jest.ts
  28. 0 122
      public/app/features/dashboard/specs/share_modal_ctrl_specs.ts
  29. 2 2
      public/app/features/dashlinks/module.ts
  30. 62 41
      public/app/features/org/partials/profile.html
  31. 10 0
      public/app/features/org/profile_ctrl.ts
  32. 4 0
      public/app/features/panellinks/link_srv.ts
  33. 6 1
      public/app/features/panellinks/module.ts
  34. 4 5
      public/app/plugins/datasource/graphite/gfunc.ts
  35. 18 0
      public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
  36. 5 0
      public/app/plugins/datasource/influxdb/datasource.ts
  37. 53 0
      public/app/plugins/datasource/influxdb/specs/datasource.jest.ts
  38. 1 19
      public/app/plugins/datasource/postgres/datasource.ts
  39. 8 0
      public/app/plugins/datasource/postgres/partials/config.html
  40. 682 651
      public/app/plugins/panel/graph/graph.ts
  41. 1 0
      public/app/plugins/panel/graph/module.ts
  42. 518 0
      public/app/plugins/panel/graph/specs/graph.jest.ts
  43. 0 454
      public/app/plugins/panel/graph/specs/graph_specs.ts
  44. 361 350
      public/app/plugins/panel/heatmap/rendering.ts
  45. 0 320
      public/app/plugins/panel/heatmap/specs/renderer_specs.ts
  46. 1 1
      public/app/plugins/panel/table/module.html
  47. 4 0
      public/app/plugins/panel/table/renderer.ts
  48. 2 0
      public/app/routes/dashboard_loaders.ts
  49. 4 4
      public/views/index.template.html
  50. 0 4
      scripts/grunt/default_task.js
  51. 0 22
      scripts/grunt/options/jscs.js
  52. 0 20
      scripts/grunt/options/jshint.js
  53. 0 45
      tasks/options/copy.js
  54. 28 340
      yarn.lock

+ 1 - 1
.bra.toml

@@ -9,7 +9,7 @@ watch_dirs = [
 	"$WORKDIR/public/views",
 	"$WORKDIR/conf",
 ]
-watch_exts = [".go", ".ini", ".toml"]
+watch_exts = [".go", ".ini", ".toml", ".template.html"]
 build_delay = 1500
 cmds = [
   ["go", "run", "build.go", "-dev", "build-server"],

+ 1 - 0
.circleci/config.yml

@@ -104,6 +104,7 @@ jobs:
       - run:
           name: yarn install
           command: 'yarn install --pure-lockfile --no-progress'
+          no_output_timeout: 15m
       - save_cache:
           key: dependency-cache-{{ checksum "yarn.lock" }}
           paths:

+ 0 - 13
.jscs.json

@@ -1,13 +0,0 @@
-{
-    "disallowImplicitTypeConversion": ["string"],
-    "disallowKeywords": ["with"],
-    "disallowMultipleLineBreaks": true,
-    "disallowMixedSpacesAndTabs": true,
-    "disallowTrailingWhitespace": true,
-    "requireSpacesInFunctionExpression": {
-        "beforeOpeningCurlyBrace": true
-    },
-    "disallowSpacesInsideArrayBrackets": true,
-    "disallowSpacesInsideParentheses": true,
-    "validateIndentation": 2
-}

+ 0 - 37
.jshintrc

@@ -1,37 +0,0 @@
-{
-  "browser": true,
-  "esversion": 6,
-  "bitwise":false,
-  "curly": true,
-  "eqnull": true,
-  "strict": false,
-  "devel": true,
-  "eqeqeq": true,
-  "forin": false,
-  "immed": true,
-  "supernew": true,
-  "expr": true,
-  "indent": 2,
-  "latedef": false,
-  "newcap": true,
-  "noarg": true,
-  "noempty": true,
-  "undef": true,
-  "boss": true,
-  "trailing": true,
-  "laxbreak": true,
-  "laxcomma": true,
-  "sub": true,
-  "unused": true,
-  "maxdepth": 6,
-  "maxlen": 140,
-
-  "globals": {
-    "System": true,
-    "Promise": true,
-    "define": true,
-    "require": true,
-    "Chromath": false,
-    "setImmediate": true
-  }
-}

+ 11 - 2
CHANGELOG.md

@@ -5,13 +5,16 @@
 * **LDAP**: Define Grafana Admin permission in ldap group mappings [#2469](https://github.com/grafana/grafana/issues/2496), PR [#12622](https://github.com/grafana/grafana/issues/12622)
 * **Cloudwatch**: CloudWatch GetMetricData support [#11487](https://github.com/grafana/grafana/issues/11487), thx [@mtanda](https://github.com/mtanda)
 * **Configuration**: Allow auto-assigning users to specific organization (other than Main. Org) [#1823](https://github.com/grafana/grafana/issues/1823) [#12801](https://github.com/grafana/grafana/issues/12801), thx [@gzzo](https://github.com/gzzo) and [@ofosos](https://github.com/ofosos)
+* **Profile**: List teams that the user is member of in current/active organization [#12476](https://github.com/grafana/grafana/issues/12476)
+* **LDAP**: Client certificates support [#12805](https://github.com/grafana/grafana/issues/12805), thx [@nyxi](https://github.com/nyxi)
+* **Postgres**: TimescaleDB support, e.g. use `time_bucket` for grouping by time when option enabled [#12680](https://github.com/grafana/grafana/pull/12680), thx [svenklemm](https://github.com/svenklemm)
 
 ### Minor
 
 * **Api**: Delete nonexistent datasource should return 404 [#12313](https://github.com/grafana/grafana/issues/12313), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Dashboard**: Fix selecting current dashboard from search should not reload dashboard [#12248](https://github.com/grafana/grafana/issues/12248)
+* **Dashboard**: Use uid when linking to dashboards internally in a dashboard [#10705](https://github.com/grafana/grafana/issues/10705)
 * **Singlestat**: Make colorization of prefix and postfix optional in singlestat [#11892](https://github.com/grafana/grafana/pull/11892), thx [@ApsOps](https://github.com/ApsOps)
-* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Prometheus**: Fix graph panel bar width issue in aligned prometheus queries [#12379](https://github.com/grafana/grafana/issues/12379)
 * **Prometheus**: Heatmap - fix unhandled error when some points are missing [#12484](https://github.com/grafana/grafana/issues/12484)
 * **Prometheus**: Add $__interval, $__interval_ms, $__range, $__range_s & $__range_ms support for dashboard and template queries [#12597](https://github.com/grafana/grafana/issues/12597) [#12882](https://github.com/grafana/grafana/issues/12882), thx [@roidelapluie](https://github.com/roidelapluie)
@@ -25,7 +28,7 @@
 * **Postgres**: Escape ssl mode parameter in connectionstring [#12644](https://github.com/grafana/grafana/issues/12644), thx [@yogyrahmawan](https://github.com/yogyrahmawan)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
 * **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
-* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
+* **Alerting**: Fix rendering timeout which could cause notifications to not be sent due to rendering timing out [#12151](https://github.com/grafana/grafana/issues/12151)
 * **Cloudwatch**: Improved error handling [#12489](https://github.com/grafana/grafana/issues/12489), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: AppSync metrics and dimensions [#12300](https://github.com/grafana/grafana/issues/12300), thx [@franciscocpg](https://github.com/franciscocpg)
 * **Cloudwatch**: Direct Connect metrics and dimensions [#12762](https://github.com/grafana/grafana/pulls/12762), thx [@mindriot88](https://github.com/mindriot88)
@@ -33,14 +36,20 @@
 * **Cloudwatch**: Add new Redshift metrics and dimensions [#12063](https://github.com/grafana/grafana/pulls/12063), thx [@A21z](https://github.com/A21z)
 * **Table**: Adjust header contrast for the light theme [#12668](https://github.com/grafana/grafana/issues/12668)
 * **Table**: Fix link color when using light theme and thresholds in use [#12766](https://github.com/grafana/grafana/issues/12766)
+om/grafana/grafana/issues/12668)
+* **Table**: Fix for useless horizontal scrollbar for table panel [#9964](https://github.com/grafana/grafana/issues/9964)
+* **Table**: Make table sorting stable when null values exist [#12362](https://github.com/grafana/grafana/pull/12362), thx [@bz2](https://github.com/bz2)
 * **Elasticsearch**: For alerting/backend, support having index name to the right of pattern in index pattern [#12731](https://github.com/grafana/grafana/issues/12731)
 * **OAuth**: Fix overriding tls_skip_verify_insecure using environment variable [#12747](https://github.com/grafana/grafana/issues/12747), thx [@jangaraj](https://github.com/jangaraj)
 * **Units**: Change units to include characters for power of 2 and 3 [#12744](https://github.com/grafana/grafana/pull/12744), thx [@Worty](https://github.com/Worty)
+* **Units**: Polish złoty currency [#12691](https://github.com/grafana/grafana/pull/12691), thx [@mwegrzynek](https://github.com/mwegrzynek)
 * **Graph**: Option to hide series from tooltip [#3341](https://github.com/grafana/grafana/issues/3341), thx [@mtanda](https://github.com/mtanda)
 * **UI**: Fix iOS home screen "app" icon and Windows 10 app experience [#12752](https://github.com/grafana/grafana/issues/12752), thx [@andig](https://github.com/andig)
 * **Datasource**: Fix UI issue with secret fields after updating datasource [#11270](https://github.com/grafana/grafana/issues/11270)
 * **Plugins**: Convert URL-like text to links in plugins readme [#12843](https://github.com/grafana/grafana/pull/12843), thx [pgiraud](https://github.com/pgiraud)
 * **Docker**: Make it possible to set a specific plugin url [#12861](https://github.com/grafana/grafana/pull/12861), thx [ClementGautier](https://github.com/ClementGautier)
+* **Graphite**: Fix for quoting of int function parameters (when using variables) [#11927](https://github.com/grafana/grafana/pull/11927)
+* **InfluxDB**: Support timeFilter in query templating for InfluxDB [#12598](https://github.com/grafana/grafana/pull/12598), thx [kichristensen](https://github.com/kichristensen)
 
 ### Breaking changes
 

+ 0 - 1
Gruntfile.js

@@ -1,4 +1,3 @@
-/* jshint node:true */
 'use strict';
 module.exports = function (grunt) {
   var os = require('os');

+ 3 - 0
conf/defaults.ini

@@ -315,6 +315,9 @@ api_url =
 team_ids =
 allowed_organizations =
 tls_skip_verify_insecure = false
+tls_client_cert =
+tls_client_key =
+tls_client_ca =
 
 #################################### Basic Auth ##########################
 [auth.basic]

+ 3 - 0
conf/ldap.toml

@@ -15,6 +15,9 @@ start_tls = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"

+ 4 - 0
conf/sample.ini

@@ -272,6 +272,10 @@ log_queries =
 ;api_url = https://foo.bar/user
 ;team_ids =
 ;allowed_organizations =
+;tls_skip_verify_insecure = false
+;tls_client_cert =
+;tls_client_key =
+;tls_client_ca =
 
 #################################### Grafana.com Auth ####################
 [auth.grafana_com]

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

@@ -31,6 +31,7 @@ Name | Description
 *User* | Database user's login/username
 *Password* | Database user's password
 *SSL Mode* | This option determines whether or with what priority a secure SSL TCP/IP connection will be negotiated with the server.
+*TimescaleDB* | With this option enabled Grafana will use TimescaleDB features, e.g. use ```time_bucket``` for grouping by time (only available in Grafana 5.3+).
 
 ### Database User Permissions (Important!)
 
@@ -289,4 +290,5 @@ datasources:
       password: "Password!"
     jsonData:
       sslmode: "disable" # disable/require/verify-ca/verify-full
+      timescaledb: false
 ```

+ 33 - 0
docs/sources/http_api/user.md

@@ -363,6 +363,39 @@ Content-Type: application/json
 ]
 ```
 
+## Teams that the actual User is member of
+
+`GET /api/user/teams`
+
+Return a list of all teams that the current user is member of.
+
+**Example Request**:
+
+```http
+GET /api/user/teams HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
+```
+
+**Example Response**:
+
+```http
+HTTP/1.1 200
+Content-Type: application/json
+
+[
+  {
+    "id": 1,
+    "orgId": 1,
+    "name": "MyTestTeam",
+    "email": "",
+    "avatarUrl": "\/avatar\/3f49c15916554246daa714b9bd0ee398",
+    "memberCount": 1
+  }
+]
+```
+
 ## Star a dashboard
 
 `POST /api/user/stars/dashboard/:dashboardId`

+ 3 - 0
docs/sources/installation/ldap.md

@@ -40,6 +40,9 @@ start_tls = false
 ssl_skip_verify = false
 # set to the path to your root CA certificate or leave unset to use system defaults
 # root_ca_cert = "/path/to/certificate.crt"
+# Authentication against LDAP servers requiring client certificates
+# client_cert = "/path/to/client.crt"
+# client_key = "/path/to/client.key"
 
 # Search user bind dn
 bind_dn = "cn=admin,dc=grafana,dc=org"

+ 0 - 3
package.json

@@ -45,9 +45,7 @@
     "grunt-contrib-concat": "^1.0.1",
     "grunt-contrib-copy": "~1.0.0",
     "grunt-contrib-cssmin": "~1.0.2",
-    "grunt-contrib-jshint": "~1.1.0",
     "grunt-exec": "^1.0.1",
-    "grunt-jscs": "3.0.1",
     "grunt-karma": "~2.0.0",
     "grunt-notify": "^0.4.5",
     "grunt-postcss": "^0.8.0",
@@ -60,7 +58,6 @@
     "html-webpack-plugin": "^3.2.0",
     "husky": "^0.14.3",
     "jest": "^22.0.4",
-    "jshint-stylish": "~2.2.1",
     "karma": "1.7.0",
     "karma-chrome-launcher": "~2.2.0",
     "karma-expect": "~1.1.3",

+ 1 - 0
pkg/api/api.go

@@ -120,6 +120,7 @@ func (hs *HTTPServer) registerRoutes() {
 			userRoute.Put("/", bind(m.UpdateUserCommand{}), Wrap(UpdateSignedInUser))
 			userRoute.Post("/using/:id", Wrap(UserSetUsingOrg))
 			userRoute.Get("/orgs", Wrap(GetSignedInUserOrgList))
+			userRoute.Get("/teams", Wrap(GetSignedInUserTeamList))
 
 			userRoute.Post("/stars/dashboard/:id", Wrap(StarDashboard))
 			userRoute.Delete("/stars/dashboard/:id", Wrap(UnstarDashboard))

+ 15 - 0
pkg/api/user.go

@@ -111,6 +111,21 @@ func GetSignedInUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.UserId)
 }
 
+// GET /api/user/teams
+func GetSignedInUserTeamList(c *m.ReqContext) Response {
+	query := m.GetTeamsByUserQuery{OrgId: c.OrgId, UserId: c.UserId}
+
+	if err := bus.Dispatch(&query); err != nil {
+		return Error(500, "Failed to get user teams", err)
+	}
+
+	for _, team := range query.Result {
+		team.AvatarUrl = dtos.GetGravatarUrlWithDefault(team.Email, team.Name)
+	}
+
+	return JSON(200, query.Result)
+}
+
 // GET /api/user/:id/orgs
 func GetUserOrgList(c *m.ReqContext) Response {
 	return getUserOrgList(c.ParamsInt64(":id"))

+ 10 - 0
pkg/login/ldap.go

@@ -59,6 +59,13 @@ func (a *ldapAuther) Dial() error {
 			}
 		}
 	}
+	var clientCert tls.Certificate
+	if a.server.ClientCert != "" && a.server.ClientKey != "" {
+		clientCert, err = tls.LoadX509KeyPair(a.server.ClientCert, a.server.ClientKey)
+		if err != nil {
+			return err
+		}
+	}
 	for _, host := range strings.Split(a.server.Host, " ") {
 		address := fmt.Sprintf("%s:%d", host, a.server.Port)
 		if a.server.UseSSL {
@@ -67,6 +74,9 @@ func (a *ldapAuther) Dial() error {
 				ServerName:         host,
 				RootCAs:            certPool,
 			}
+			if len(clientCert.Certificate) > 0 {
+				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
+			}
 			if a.server.StartTLS {
 				a.conn, err = ldap.Dial("tcp", address)
 				if err == nil {

+ 2 - 0
pkg/login/ldap_settings.go

@@ -21,6 +21,8 @@ type LdapServerConf struct {
 	StartTLS      bool             `toml:"start_tls"`
 	SkipVerifySSL bool             `toml:"ssl_skip_verify"`
 	RootCACert    string           `toml:"root_ca_cert"`
+	ClientCert    string           `toml:"client_cert"`
+	ClientKey     string           `toml:"client_key"`
 	BindDN        string           `toml:"bind_dn"`
 	BindPassword  string           `toml:"bind_password"`
 	Attr          LdapAttributeMap `toml:"attributes"`

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

@@ -3,7 +3,6 @@ package alerting
 import (
 	"errors"
 	"fmt"
-	"time"
 
 	"golang.org/x/sync/errgroup"
 
@@ -81,7 +80,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
 	renderOpts := rendering.Opts{
 		Width:   1000,
 		Height:  500,
-		Timeout: time.Second * 30,
+		Timeout: alertTimeout / 2,
 		OrgId:   context.Rule.OrgId,
 		OrgRole: m.ROLE_ADMIN,
 	}

+ 11 - 5
pkg/tsdb/postgres/macros.go

@@ -14,12 +14,13 @@ const rsIdentifier = `([_a-zA-Z0-9]+)`
 const sExpr = `\$` + rsIdentifier + `\(([^\)]*)\)`
 
 type postgresMacroEngine struct {
-	timeRange *tsdb.TimeRange
-	query     *tsdb.Query
+	timeRange   *tsdb.TimeRange
+	query       *tsdb.Query
+	timescaledb bool
 }
 
-func newPostgresMacroEngine() tsdb.SqlMacroEngine {
-	return &postgresMacroEngine{}
+func newPostgresMacroEngine(timescaledb bool) tsdb.SqlMacroEngine {
+	return &postgresMacroEngine{timescaledb: timescaledb}
 }
 
 func (m *postgresMacroEngine) Interpolate(query *tsdb.Query, timeRange *tsdb.TimeRange, sql string) (string, error) {
@@ -118,7 +119,12 @@ func (m *postgresMacroEngine) evaluateMacro(name string, args []string) (string,
 				return "", err
 			}
 		}
-		return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+
+		if m.timescaledb {
+			return fmt.Sprintf("time_bucket('%vs',%s)", interval.Seconds(), args[0]), nil
+		} else {
+			return fmt.Sprintf("floor(extract(epoch from %s)/%v)*%v", args[0], interval.Seconds(), interval.Seconds()), nil
+		}
 	case "__timeGroupAlias":
 		tg, err := m.evaluateMacro("__timeGroup", args)
 		if err == nil {

+ 20 - 1
pkg/tsdb/postgres/macros_test.go

@@ -12,7 +12,10 @@ import (
 
 func TestMacroEngine(t *testing.T) {
 	Convey("MacroEngine", t, func() {
-		engine := newPostgresMacroEngine()
+		timescaledbEnabled := false
+		engine := newPostgresMacroEngine(timescaledbEnabled)
+		timescaledbEnabled = true
+		engineTS := newPostgresMacroEngine(timescaledbEnabled)
 		query := &tsdb.Query{}
 
 		Convey("Given a time range between 2018-04-12 00:00 and 2018-04-12 00:05", func() {
@@ -83,6 +86,22 @@ func TestMacroEngine(t *testing.T) {
 				So(sql2, ShouldEqual, sql+" AS \"time\"")
 			})
 
+			Convey("interpolate __timeGroup function with TimescaleDB enabled", func() {
+
+				sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
+			})
+
+			Convey("interpolate __timeGroup function with spaces between args and TimescaleDB enabled", func() {
+
+				sql, err := engineTS.Interpolate(query, timeRange, "GROUP BY $__timeGroup(time_column , '5m')")
+				So(err, ShouldBeNil)
+
+				So(sql, ShouldEqual, "GROUP BY time_bucket('300s',time_column)")
+			})
+
 			Convey("interpolate __timeTo function", func() {
 				sql, err := engine.Interpolate(query, timeRange, "select $__timeTo(time_column)")
 				So(err, ShouldBeNil)

+ 3 - 1
pkg/tsdb/postgres/postgres.go

@@ -32,7 +32,9 @@ func newPostgresQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndp
 		log: logger,
 	}
 
-	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(), logger)
+	timescaledb := datasource.JsonData.Get("timescaledb").MustBool(false)
+
+	return tsdb.NewSqlQueryEndpoint(&config, &rowTransformer, newPostgresMacroEngine(timescaledb), logger)
 }
 
 func generateConnectionString(datasource *models.DataSource) string {

+ 1 - 1
pkg/tsdb/postgres/postgres_test.go

@@ -27,7 +27,7 @@ import (
 // use to verify that the generated data are vizualized as expected, see
 // devenv/README.md for setup instructions.
 func TestPostgres(t *testing.T) {
-	// change to true to run the MySQL tests
+	// change to true to run the PostgreSQL tests
 	runPostgresTests := false
 	// runPostgresTests := true
 

+ 3 - 2
public/app/containers/Teams/TeamMembers.tsx

@@ -69,8 +69,9 @@ export class TeamMembers extends React.Component<Props, State> {
 
   render() {
     const { newTeamMember, isAdding } = this.state;
-    const members = this.props.team.members.values();
+    const members = this.props.team.filteredMembers;
     const newTeamMemberValue = newTeamMember && newTeamMember.id.toString();
+    const { team } = this.props;
 
     return (
       <div>
@@ -81,7 +82,7 @@ export class TeamMembers extends React.Component<Props, State> {
                 type="text"
                 className="gf-form-input"
                 placeholder="Search members"
-                value={''}
+                value={team.search}
                 onChange={this.onSearchQueryChange}
               />
               <i className="gf-form-input-icon fa fa-search" />

+ 1 - 0
public/app/core/components/help/help.ts

@@ -25,6 +25,7 @@ export class HelpCtrl {
         { keys: ['d', 'k'], description: 'Toggle kiosk mode (hides top nav)' },
         { keys: ['d', 'E'], description: 'Expand all rows' },
         { keys: ['d', 'C'], description: 'Collapse all rows' },
+        { keys: ['d', 'a'], description: 'Toggle auto fit panels (experimental feature)' },
         { keys: ['mod+o'], description: 'Toggle shared graph crosshair' },
       ],
       'Focused Panel': [

+ 3 - 12
public/app/core/services/keybindingSrv.ts

@@ -15,14 +15,7 @@ export class KeybindingSrv {
   timepickerOpen = false;
 
   /** @ngInject */
-  constructor(
-    private $rootScope,
-    private $location,
-    private datasourceSrv,
-    private timeSrv,
-    private contextSrv,
-    private $route
-  ) {
+  constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv, private contextSrv) {
     // clear out all shortcuts on route change
     $rootScope.$on('$routeChangeSuccess', () => {
       Mousetrap.reset();
@@ -269,10 +262,8 @@ export class KeybindingSrv {
 
     //Autofit panels
     this.bind('d a', () => {
-      this.$location.search('autofitpanels', this.$location.search().autofitpanels ? null : true);
-      //Force reload
-
-      this.$route.reload();
+      // this has to be a full page reload
+      window.location.href = window.location.href + '&autofitpanels';
     });
   }
 }

+ 111 - 113
public/app/features/dashboard/shareModalCtrl.ts

@@ -2,120 +2,118 @@ import angular from 'angular';
 import config from 'app/core/config';
 import moment from 'moment';
 
-export class ShareModalCtrl {
-  /** @ngInject */
-  constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
-    $scope.options = {
-      forCurrent: true,
-      includeTemplateVars: true,
-      theme: 'current',
-    };
-    $scope.editor = { index: $scope.tabIndex || 0 };
-
-    $scope.init = function() {
-      $scope.modeSharePanel = $scope.panel ? true : false;
-
-      $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
-
-      if ($scope.modeSharePanel) {
-        $scope.modalTitle = 'Share Panel';
-        $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' });
-      } else {
-        $scope.modalTitle = 'Share';
-      }
-
-      if (!$scope.dashboard.meta.isSnapshot) {
-        $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' });
-      }
-
-      if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) {
-        $scope.tabs.push({ title: 'Export', src: 'shareExport.html' });
-      }
-
-      $scope.buildUrl();
-    };
-
-    $scope.buildUrl = function() {
-      var baseUrl = $location.absUrl();
-      var queryStart = baseUrl.indexOf('?');
-
-      if (queryStart !== -1) {
-        baseUrl = baseUrl.substring(0, queryStart);
-      }
-
-      var params = angular.copy($location.search());
-
-      var range = timeSrv.timeRange();
-      params.from = range.from.valueOf();
-      params.to = range.to.valueOf();
-      params.orgId = config.bootData.user.orgId;
-
-      if ($scope.options.includeTemplateVars) {
-        templateSrv.fillVariableValuesForUrl(params);
-      }
-
-      if (!$scope.options.forCurrent) {
-        delete params.from;
-        delete params.to;
-      }
-
-      if ($scope.options.theme !== 'current') {
-        params.theme = $scope.options.theme;
-      }
-
-      if ($scope.modeSharePanel) {
-        params.panelId = $scope.panel.id;
-        params.fullscreen = true;
-      } else {
-        delete params.panelId;
-        delete params.fullscreen;
-      }
-
-      $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
-
-      var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
-      soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
+/** @ngInject */
+export function ShareModalCtrl($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) {
+  $scope.options = {
+    forCurrent: true,
+    includeTemplateVars: true,
+    theme: 'current',
+  };
+  $scope.editor = { index: $scope.tabIndex || 0 };
+
+  $scope.init = function() {
+    $scope.modeSharePanel = $scope.panel ? true : false;
+
+    $scope.tabs = [{ title: 'Link', src: 'shareLink.html' }];
+
+    if ($scope.modeSharePanel) {
+      $scope.modalTitle = 'Share Panel';
+      $scope.tabs.push({ title: 'Embed', src: 'shareEmbed.html' });
+    } else {
+      $scope.modalTitle = 'Share';
+    }
+
+    if (!$scope.dashboard.meta.isSnapshot) {
+      $scope.tabs.push({ title: 'Snapshot', src: 'shareSnapshot.html' });
+    }
+
+    if (!$scope.dashboard.meta.isSnapshot && !$scope.modeSharePanel) {
+      $scope.tabs.push({ title: 'Export', src: 'shareExport.html' });
+    }
+
+    $scope.buildUrl();
+  };
+
+  $scope.buildUrl = function() {
+    var baseUrl = $location.absUrl();
+    var queryStart = baseUrl.indexOf('?');
+
+    if (queryStart !== -1) {
+      baseUrl = baseUrl.substring(0, queryStart);
+    }
+
+    var params = angular.copy($location.search());
+
+    var range = timeSrv.timeRange();
+    params.from = range.from.valueOf();
+    params.to = range.to.valueOf();
+    params.orgId = config.bootData.user.orgId;
+
+    if ($scope.options.includeTemplateVars) {
+      templateSrv.fillVariableValuesForUrl(params);
+    }
+
+    if (!$scope.options.forCurrent) {
+      delete params.from;
+      delete params.to;
+    }
+
+    if ($scope.options.theme !== 'current') {
+      params.theme = $scope.options.theme;
+    }
+
+    if ($scope.modeSharePanel) {
+      params.panelId = $scope.panel.id;
+      params.fullscreen = true;
+    } else {
+      delete params.panelId;
       delete params.fullscreen;
-      delete params.edit;
-      soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
-
-      $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
-
-      $scope.imageUrl = soloUrl.replace(
-        config.appSubUrl + '/dashboard-solo/',
-        config.appSubUrl + '/render/dashboard-solo/'
-      );
-      $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
-      $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone();
-    };
-
-    // This function will try to return the proper full name of the local timezone
-    // Chrome does not handle the timezone offset (but phantomjs does)
-    $scope.getLocalTimeZone = function() {
-      let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
-
-      // Older browser does not the internationalization API
-      if (!(<any>window).Intl) {
-        return utcOffset;
-      }
-
-      const dateFormat = (<any>window).Intl.DateTimeFormat();
-      if (!dateFormat.resolvedOptions) {
-        return utcOffset;
-      }
-
-      const options = dateFormat.resolvedOptions();
-      if (!options.timeZone) {
-        return utcOffset;
-      }
-
-      return '&tz=' + encodeURIComponent(options.timeZone);
-    };
-
-    $scope.getShareUrl = function() {
-      return $scope.shareUrl;
-    };
-  }
+    }
+
+    $scope.shareUrl = linkSrv.addParamsToUrl(baseUrl, params);
+
+    var soloUrl = baseUrl.replace(config.appSubUrl + '/dashboard/', config.appSubUrl + '/dashboard-solo/');
+    soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/');
+    delete params.fullscreen;
+    delete params.edit;
+    soloUrl = linkSrv.addParamsToUrl(soloUrl, params);
+
+    $scope.iframeHtml = '<iframe src="' + soloUrl + '" width="450" height="200" frameborder="0"></iframe>';
+
+    $scope.imageUrl = soloUrl.replace(
+      config.appSubUrl + '/dashboard-solo/',
+      config.appSubUrl + '/render/dashboard-solo/'
+    );
+    $scope.imageUrl = $scope.imageUrl.replace(config.appSubUrl + '/d-solo/', config.appSubUrl + '/render/d-solo/');
+    $scope.imageUrl += '&width=1000&height=500' + $scope.getLocalTimeZone();
+  };
+
+  // This function will try to return the proper full name of the local timezone
+  // Chrome does not handle the timezone offset (but phantomjs does)
+  $scope.getLocalTimeZone = function() {
+    let utcOffset = '&tz=UTC' + encodeURIComponent(moment().format('Z'));
+
+    // Older browser does not the internationalization API
+    if (!(<any>window).Intl) {
+      return utcOffset;
+    }
+
+    const dateFormat = (<any>window).Intl.DateTimeFormat();
+    if (!dateFormat.resolvedOptions) {
+      return utcOffset;
+    }
+
+    const options = dateFormat.resolvedOptions();
+    if (!options.timeZone) {
+      return utcOffset;
+    }
+
+    return '&tz=' + encodeURIComponent(options.timeZone);
+  };
+
+  $scope.getShareUrl = function() {
+    return $scope.shareUrl;
+  };
 }
 
 angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl);

+ 150 - 0
public/app/features/dashboard/specs/share_modal_ctrl.jest.ts

@@ -0,0 +1,150 @@
+import '../shareModalCtrl';
+import { ShareModalCtrl } from '../shareModalCtrl';
+import config from 'app/core/config';
+import { LinkSrv } from 'app/features/panellinks/link_srv';
+
+describe('ShareModalCtrl', () => {
+  var ctx = <any>{
+    timeSrv: {
+      timeRange: () => {
+        return { from: new Date(1000), to: new Date(2000) };
+      },
+    },
+    $location: {
+      absUrl: () => 'http://server/#!/test',
+      search: () => {
+        return { from: '', to: '' };
+      },
+    },
+    scope: {
+      dashboard: {
+        meta: {
+          isSnapshot: true,
+        },
+      },
+    },
+    templateSrv: {
+      fillVariableValuesForUrl: () => {},
+    },
+  };
+
+  (<any>window).Intl.DateTimeFormat = () => {
+    return {
+      resolvedOptions: () => {
+        return { timeZone: 'UTC' };
+      },
+    };
+  };
+
+  beforeEach(() => {
+    config.bootData = {
+      user: {
+        orgId: 1,
+      },
+    };
+
+    ctx.ctrl = new ShareModalCtrl(
+      ctx.scope,
+      {},
+      ctx.$location,
+      {},
+      ctx.timeSrv,
+      ctx.templateSrv,
+      new LinkSrv({}, ctx.stimeSrv)
+    );
+  });
+
+  describe('shareUrl with current time range and panel', () => {
+    it('should generate share url absolute time', () => {
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen');
+    });
+
+    it('should generate render url', () => {
+      ctx.$location.absUrl = () => 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
+
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
+      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
+      expect(ctx.scope.imageUrl).toContain(base + params);
+    });
+
+    it('should generate render url for scripted dashboard', () => {
+      ctx.$location.absUrl = () => 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
+
+      ctx.scope.panel = { id: 22 };
+
+      ctx.scope.init();
+      var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
+      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
+      expect(ctx.scope.imageUrl).toContain(base + params);
+    });
+
+    it('should remove panel id when no panel in scope', () => {
+      ctx.$location.absUrl = () => 'http://server/#!/test';
+      ctx.scope.options.forCurrent = true;
+      ctx.scope.panel = null;
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1');
+    });
+
+    it('should add theme when specified', () => {
+      ctx.scope.options.theme = 'light';
+      ctx.scope.panel = null;
+
+      ctx.scope.init();
+      expect(ctx.scope.shareUrl).toBe('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
+    });
+
+    it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.search = () => {
+        return { fullscreen: true, edit: true };
+      };
+      ctx.$location.absUrl = () => 'http://server/#!/test?fullscreen&edit';
+      ctx.scope.modeSharePanel = true;
+      ctx.scope.panel = { id: 1 };
+
+      ctx.scope.buildUrl();
+
+      expect(ctx.scope.shareUrl).toContain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1');
+      expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
+    });
+
+    it('should remove edit from image url when is first param in querystring and modeSharePanel is true', () => {
+      ctx.$location.search = () => {
+        return { edit: true, fullscreen: true };
+      };
+      ctx.$location.absUrl = () => 'http://server/#!/test?edit&fullscreen';
+      ctx.scope.modeSharePanel = true;
+      ctx.scope.panel = { id: 1 };
+
+      ctx.scope.buildUrl();
+
+      expect(ctx.scope.shareUrl).toContain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1');
+      expect(ctx.scope.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
+    });
+
+    it('should include template variables in url', () => {
+      ctx.$location.search = () => {
+        return {};
+      };
+      ctx.$location.absUrl = () => 'http://server/#!/test';
+      ctx.scope.options.includeTemplateVars = true;
+
+      ctx.templateSrv.fillVariableValuesForUrl = function(params) {
+        params['var-app'] = 'mupp';
+        params['var-server'] = 'srv-01';
+      };
+
+      ctx.scope.buildUrl();
+      expect(ctx.scope.shareUrl).toContain(
+        'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01'
+      );
+    });
+  });
+});

+ 0 - 122
public/app/features/dashboard/specs/share_modal_ctrl_specs.ts

@@ -1,122 +0,0 @@
-import { describe, beforeEach, it, expect, sinon, angularMocks } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
-import '../shareModalCtrl';
-import config from 'app/core/config';
-import 'app/features/panellinks/link_srv';
-
-describe('ShareModalCtrl', function() {
-  var ctx = new helpers.ControllerTestContext();
-
-  function setTime(range) {
-    ctx.timeSrv.timeRange = sinon.stub().returns(range);
-  }
-
-  beforeEach(function() {
-    config.bootData = {
-      user: {
-        orgId: 1,
-      },
-    };
-  });
-
-  setTime({ from: new Date(1000), to: new Date(2000) });
-
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(
-    angularMocks.module(function($compileProvider) {
-      $compileProvider.preAssignBindingsEnabled(true);
-    })
-  );
-
-  beforeEach(ctx.providePhase());
-
-  beforeEach(ctx.createControllerPhase('ShareModalCtrl'));
-
-  describe('shareUrl with current time range and panel', function() {
-    it('should generate share url absolute time', function() {
-      ctx.$location.path('/test');
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&panelId=22&fullscreen');
-    });
-
-    it('should generate render url', function() {
-      ctx.$location.$$absUrl = 'http://dashboards.grafana.com/d/abcdefghi/my-dash';
-
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      var base = 'http://dashboards.grafana.com/render/d-solo/abcdefghi/my-dash';
-      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
-      expect(ctx.scope.imageUrl).to.contain(base + params);
-    });
-
-    it('should generate render url for scripted dashboard', function() {
-      ctx.$location.$$absUrl = 'http://dashboards.grafana.com/dashboard/script/my-dash.js';
-
-      ctx.scope.panel = { id: 22 };
-
-      ctx.scope.init();
-      var base = 'http://dashboards.grafana.com/render/dashboard-solo/script/my-dash.js';
-      var params = '?from=1000&to=2000&orgId=1&panelId=22&width=1000&height=500&tz=UTC';
-      expect(ctx.scope.imageUrl).to.contain(base + params);
-    });
-
-    it('should remove panel id when no panel in scope', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.forCurrent = true;
-      ctx.scope.panel = null;
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1');
-    });
-
-    it('should add theme when specified', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.theme = 'light';
-      ctx.scope.panel = null;
-
-      ctx.scope.init();
-      expect(ctx.scope.shareUrl).to.be('http://server/#!/test?from=1000&to=2000&orgId=1&theme=light');
-    });
-
-    it('should remove fullscreen from image url when is first param in querystring and modeSharePanel is true', function() {
-      ctx.$location.url('/test?fullscreen&edit');
-      ctx.scope.modeSharePanel = true;
-      ctx.scope.panel = { id: 1 };
-
-      ctx.scope.buildUrl();
-
-      expect(ctx.scope.shareUrl).to.contain('?fullscreen&edit&from=1000&to=2000&orgId=1&panelId=1');
-      expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
-    });
-
-    it('should remove edit from image url when is first param in querystring and modeSharePanel is true', function() {
-      ctx.$location.url('/test?edit&fullscreen');
-      ctx.scope.modeSharePanel = true;
-      ctx.scope.panel = { id: 1 };
-
-      ctx.scope.buildUrl();
-
-      expect(ctx.scope.shareUrl).to.contain('?edit&fullscreen&from=1000&to=2000&orgId=1&panelId=1');
-      expect(ctx.scope.imageUrl).to.contain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC');
-    });
-
-    it('should include template variables in url', function() {
-      ctx.$location.path('/test');
-      ctx.scope.options.includeTemplateVars = true;
-
-      ctx.templateSrv.fillVariableValuesForUrl = function(params) {
-        params['var-app'] = 'mupp';
-        params['var-server'] = 'srv-01';
-      };
-
-      ctx.scope.buildUrl();
-      expect(ctx.scope.shareUrl).to.be(
-        'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01'
-      );
-    });
-  });
-});

+ 2 - 2
public/app/features/dashlinks/module.ts

@@ -144,8 +144,8 @@ export class DashLinksContainerCtrl {
             if (dash.id !== currentDashId) {
               memo.push({
                 title: dash.title,
-                url: 'dashboard/' + dash.uri,
-                target: link.target,
+                url: dash.url,
+                target: link.target === '_self' ? '' : link.target,
                 icon: 'fa fa-th-large',
                 keepTime: link.keepTime,
                 includeVars: link.includeVars,

+ 62 - 41
public/app/features/org/partials/profile.html

@@ -3,53 +3,74 @@
 <div class="page-container page-body">
   <h3 class="page-sub-heading">User Profile</h3>
 
-	<form name="ctrl.userForm" class="gf-form-group">
+  <form name="ctrl.userForm" class="gf-form-group">
 
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Name</span>
-			<input class="gf-form-input max-width-22" type="text" required ng-model="ctrl.user.name" >
-		</div>
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Email</span>
-			<input class="gf-form-input max-width-22" type="email" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.email">
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Name</span>
+      <input class="gf-form-input max-width-22" type="text" required ng-model="ctrl.user.name">
+    </div>
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Email</span>
+      <input class="gf-form-input max-width-22" type="email" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.email">
       <i ng-if="ctrl.readonlyLoginFields" class="fa fa-lock gf-form-icon--right-absolute" bs-tooltip="'Login Details Locked - managed in another system.'"></i>
     </div>
-		<div class="gf-form max-width-30">
-			<span class="gf-form-label width-8">Username</span>
+    <div class="gf-form max-width-30">
+      <span class="gf-form-label width-8">Username</span>
       <input class="gf-form-input max-width-22" type="text" ng-readonly="ctrl.readonlyLoginFields" required ng-model="ctrl.user.login">
       <i ng-if="ctrl.readonlyLoginFields" class="fa fa-lock gf-form-icon--right-absolute" bs-tooltip="'Login Details Locked - managed in another system.'"></i>
     </div>
-		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
-		</div>
-	</form>
+    <div class="gf-form-button-row">
+      <button type="submit" class="btn btn-success" ng-click="ctrl.update()">Save</button>
+    </div>
+  </form>
 
-	<prefs-control mode="user"></prefs-control>
+  <prefs-control mode="user"></prefs-control>
 
-	<h3 class="page-heading" ng-show="ctrl.showOrgsList">Organizations</h3>
-  <div class="gf-form-group" ng-show="ctrl.showOrgsList">
-		<table class="filter-table form-inline">
-			<thead>
-				<tr>
-					<th>Name</th>
-					<th>Role</th>
-					<th></th>
-				</tr>
-			</thead>
-			<tbody>
-				<tr ng-repeat="org in ctrl.orgs">
-					<td>{{org.name}}</td>
-					<td>{{org.role}}</td>
-					<td class="text-right">
-						<span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
-							Current
-						</span>
-						<a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
-							Select
-						</a>
-					</td>
-				</tr>
-			</tbody>
-		</table>
-	</div>
+  <h3 class="page-heading" ng-show="ctrl.showTeamsList">Teams</h3>
+  <div class="gf-form-group" ng-show="ctrl.showTeamsList">
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th></th>
+          <th>Name</th>
+          <th>Email</th>
+          <th>Members</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="team in ctrl.teams">
+          <td class="width-4 text-center"><img class="filter-table__avatar" src={{team.avatarUrl}}></td>
+          <td>{{team.name}}</td>
+          <td>{{team.email}}</td>
+          <td>{{team.memberCount}}</td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
 
+  <h3 class="page-heading" ng-show="ctrl.showOrgsList">Organizations</h3>
+  <div class="gf-form-group" ng-show="ctrl.showOrgsList">
+    <table class="filter-table form-inline">
+      <thead>
+        <tr>
+          <th>Name</th>
+          <th>Role</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="org in ctrl.orgs">
+          <td>{{org.name}}</td>
+          <td>{{org.role}}</td>
+          <td class="text-right">
+            <span class="btn btn-primary btn-mini" ng-show="org.orgId === contextSrv.user.orgId">
+              Current
+            </span>
+            <a ng-click="ctrl.setUsingOrg(org)" class="btn btn-inverse btn-mini" ng-show="org.orgId !== contextSrv.user.orgId">
+              Select
+            </a>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>

+ 10 - 0
public/app/features/org/profile_ctrl.ts

@@ -4,8 +4,10 @@ import { coreModule } from 'app/core/core';
 export class ProfileCtrl {
   user: any;
   old_theme: any;
+  teams: any = [];
   orgs: any = [];
   userForm: any;
+  showTeamsList = false;
   showOrgsList = false;
   readonlyLoginFields = config.disableLoginForm;
   navModel: any;
@@ -13,6 +15,7 @@ export class ProfileCtrl {
   /** @ngInject **/
   constructor(private backendSrv, private contextSrv, private $location, navModelSrv) {
     this.getUser();
+    this.getUserTeams();
     this.getUserOrgs();
     this.navModel = navModelSrv.getNav('profile', 'profile-settings', 0);
   }
@@ -24,6 +27,13 @@ export class ProfileCtrl {
     });
   }
 
+  getUserTeams() {
+    this.backendSrv.get('/api/user/teams').then(teams => {
+      this.teams = teams;
+      this.showTeamsList = this.teams.length > 0;
+    });
+  }
+
   getUserOrgs() {
     this.backendSrv.get('/api/user/orgs').then(orgs => {
       this.orgs = orgs;

+ 4 - 0
public/app/features/panellinks/link_srv.ts

@@ -77,6 +77,10 @@ export class LinkSrv {
       info.target = link.targetBlank ? '_blank' : '_self';
       info.href = this.templateSrv.replace(link.url || '', scopedVars);
       info.title = this.templateSrv.replace(link.title || '', scopedVars);
+    } else if (link.url) {
+      info.href = link.url;
+      info.title = this.templateSrv.replace(link.title || '', scopedVars);
+      info.target = link.targetBlank ? '_blank' : '';
     } else if (link.dashUri) {
       info.href = 'dashboard/' + link.dashUri + '?';
       info.title = this.templateSrv.replace(link.title || '', scopedVars);

+ 6 - 1
public/app/features/panellinks/module.ts

@@ -39,7 +39,12 @@ export class PanelLinksEditorCtrl {
       backendSrv.search({ query: link.dashboard }).then(function(hits) {
         var dashboard = _.find(hits, { title: link.dashboard });
         if (dashboard) {
-          link.dashUri = dashboard.uri;
+          if (dashboard.url) {
+            link.url = dashboard.url;
+          } else {
+            // To support legacy url's
+            link.dashUri = dashboard.uri;
+          }
           link.title = dashboard.title;
         }
       });

+ 4 - 5
public/app/plugins/datasource/graphite/gfunc.ts

@@ -973,13 +973,12 @@ export class FuncInstance {
         } else if (_.get(_.last(this.def.params), 'multiple')) {
           paramType = _.get(_.last(this.def.params), 'type');
         }
-        if (paramType === 'value_or_series') {
+        // param types that should never be quoted
+        if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) {
           return value;
         }
-        if (paramType === 'boolean' && _.includes(['true', 'false'], value)) {
-          return value;
-        }
-        if (_.includes(['int', 'float', 'int_or_interval', 'node_or_tag', 'node'], paramType) && _.isFinite(+value)) {
+        // param types that might be quoted
+        if (_.includes(['int_or_interval', 'node_or_tag'], paramType) && _.isFinite(+value)) {
           return _.toString(+value);
         }
         return "'" + value + "'";

+ 18 - 0
public/app/plugins/datasource/graphite/specs/gfunc.jest.ts

@@ -55,6 +55,24 @@ describe('when rendering func instance', function() {
     expect(func.render('hello')).toEqual("movingMedian(hello, '5min')");
   });
 
+  it('should never quote boolean paramater', function() {
+    var func = gfunc.createFuncInstance('sortByName');
+    func.params[0] = '$natural';
+    expect(func.render('hello')).toEqual('sortByName(hello, $natural)');
+  });
+
+  it('should never quote int paramater', function() {
+    var func = gfunc.createFuncInstance('maximumAbove');
+    func.params[0] = '$value';
+    expect(func.render('hello')).toEqual('maximumAbove(hello, $value)');
+  });
+
+  it('should never quote node paramater', function() {
+    var func = gfunc.createFuncInstance('aliasByNode');
+    func.params[0] = '$node';
+    expect(func.render('hello')).toEqual('aliasByNode(hello, $node)');
+  });
+
   it('should handle metric param and int param and string param', function() {
     var func = gfunc.createFuncInstance('groupByNode');
     func.params[0] = 5;

+ 5 - 0
public/app/plugins/datasource/influxdb/datasource.ts

@@ -187,6 +187,11 @@ export default class InfluxDatasource {
       return this.$q.when({ results: [] });
     }
 
+    if (options && options.range) {
+      var timeFilter = this.getTimeFilter({ rangeRaw: options.range });
+      query = query.replace('$timeFilter', timeFilter);
+    }
+
     return this._influxRequest('GET', '/query', { q: query, epoch: 'ms' }, options);
   }
 

+ 53 - 0
public/app/plugins/datasource/influxdb/specs/datasource.jest.ts

@@ -0,0 +1,53 @@
+import InfluxDatasource from '../datasource';
+import $q from 'q';
+import { TemplateSrvStub } from 'test/specs/helpers';
+
+describe('InfluxDataSource', () => {
+  let ctx: any = {
+    backendSrv: {},
+    $q: $q,
+    templateSrv: new TemplateSrvStub(),
+    instanceSettings: { url: 'url', name: 'influxDb', jsonData: {} },
+  };
+
+  beforeEach(function() {
+    ctx.instanceSettings.url = '/api/datasources/proxy/1';
+    ctx.ds = new InfluxDatasource(ctx.instanceSettings, ctx.$q, ctx.backendSrv, ctx.templateSrv);
+  });
+
+  describe('When issuing metricFindQuery', () => {
+    let query = 'SELECT max(value) FROM measurement WHERE $timeFilter';
+    let queryOptions: any = {
+      range: {
+        from: '2018-01-01T00:00:00Z',
+        to: '2018-01-02T00:00:00Z',
+      },
+    };
+    let requestQuery;
+
+    beforeEach(async () => {
+      ctx.backendSrv.datasourceRequest = function(req) {
+        requestQuery = req.params.q;
+        return ctx.$q.when({
+          results: [
+            {
+              series: [
+                {
+                  name: 'measurement',
+                  columns: ['max'],
+                  values: [[1]],
+                },
+              ],
+            },
+          ],
+        });
+      };
+
+      await ctx.ds.metricFindQuery(query, queryOptions).then(function(_) {});
+    });
+
+    it('should replace $timefilter', () => {
+      expect(requestQuery).toMatch('time >= 1514764800000ms and time <= 1514851200000ms');
+    });
+  });
+});

+ 1 - 19
public/app/plugins/datasource/postgres/datasource.ts

@@ -123,25 +123,7 @@ export class PostgresDatasource {
   }
 
   testDatasource() {
-    return this.backendSrv
-      .datasourceRequest({
-        url: '/api/tsdb/query',
-        method: 'POST',
-        data: {
-          from: '5m',
-          to: 'now',
-          queries: [
-            {
-              refId: 'A',
-              intervalMs: 1,
-              maxDataPoints: 1,
-              datasourceId: this.id,
-              rawSql: 'SELECT 1',
-              format: 'table',
-            },
-          ],
-        },
-      })
+    return this.metricFindQuery('SELECT 1', {})
       .then(res => {
         return { status: 'success', message: 'Database Connection OK' };
       })

+ 8 - 0
public/app/plugins/datasource/postgres/partials/config.html

@@ -38,6 +38,14 @@
 	</div>
 </div>
 
+<h3 class="page-heading">PostgreSQL details</h3>
+
+<div class="gf-form-group">
+	<div class="gf-form">
+		<gf-form-switch class="gf-form" label="TimescaleDB" tooltip="Use TimescaleDB features (e.g., time_bucket) in Grafana" label-class="width-9" checked="ctrl.current.jsonData.timescaledb" switch-class="max-width-6"></gf-form-switch>
+	</div>
+</div>
+
 <div class="gf-form-group">
 	<div class="grafana-info-box">
 		<h5>User Permission</h5>

+ 682 - 651
public/app/plugins/panel/graph/graph.ts

@@ -21,699 +21,730 @@ import { convertToHistogramData } from './histogram';
 import { alignYLevel } from './align_yaxes';
 import config from 'app/core/config';
 
-/** @ngInject **/
-function graphDirective(timeSrv, popoverSrv, contextSrv) {
-  return {
-    restrict: 'A',
-    template: '',
-    link: function(scope, elem) {
-      var ctrl = scope.ctrl;
-      var dashboard = ctrl.dashboard;
-      var panel = ctrl.panel;
-      var annotations = [];
-      var data;
-      var plot;
-      var sortedSeries;
-      var panelWidth = 0;
-      var eventManager = new EventManager(ctrl);
-      var thresholdManager = new ThresholdManager(ctrl);
-      var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
-        return sortedSeries;
-      });
-
-      // panel events
-      ctrl.events.on('panel-teardown', () => {
-        thresholdManager = null;
-
-        if (plot) {
-          plot.destroy();
-          plot = null;
-        }
-      });
-
-      /**
-       * Split graph rendering into two parts.
-       * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
-       * (see ctrl.events.on('render') in legend.ts).
-       * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
-       */
-      ctrl.events.on('render', renderData => {
-        data = renderData || data;
-        if (!data) {
-          return;
-        }
-        annotations = ctrl.annotations || [];
-        buildFlotPairs(data);
-        const graphHeight = elem.height();
-        updateLegendValues(data, panel, graphHeight);
-
-        ctrl.events.emit('render-legend');
-      });
-
-      ctrl.events.on('legend-rendering-complete', () => {
-        render_panel();
+import { GraphCtrl } from './module';
+
+class GraphElement {
+  ctrl: GraphCtrl;
+  tooltip: any;
+  dashboard: any;
+  annotations: Array<object>;
+  panel: any;
+  plot: any;
+  sortedSeries: Array<any>;
+  data: Array<any>;
+  panelWidth: number;
+  eventManager: EventManager;
+  thresholdManager: ThresholdManager;
+
+  constructor(private scope, private elem, private timeSrv) {
+    this.ctrl = scope.ctrl;
+    this.dashboard = this.ctrl.dashboard;
+    this.panel = this.ctrl.panel;
+    this.annotations = [];
+
+    this.panelWidth = 0;
+    this.eventManager = new EventManager(this.ctrl);
+    this.thresholdManager = new ThresholdManager(this.ctrl);
+    this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => {
+      return this.sortedSeries;
+    });
+
+    // panel events
+    this.ctrl.events.on('panel-teardown', this.onPanelteardown.bind(this));
+
+    /**
+     * Split graph rendering into two parts.
+     * First, calculate series stats in buildFlotPairs() function. Then legend rendering started
+     * (see ctrl.events.on('render') in legend.ts).
+     * When legend is rendered it emits 'legend-rendering-complete' and graph rendered.
+     */
+    this.ctrl.events.on('render', this.onRender.bind(this));
+    this.ctrl.events.on('legend-rendering-complete', this.onLegendRenderingComplete.bind(this));
+
+    // global events
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), scope);
+
+    this.elem.bind('plotselected', this.onPlotSelected.bind(this));
+
+    this.elem.bind('plotclick', this.onPlotClick.bind(this));
+    scope.$on('$destroy', this.onScopeDestroy.bind(this));
+  }
+
+  onRender(renderData) {
+    this.data = renderData || this.data;
+    if (!this.data) {
+      return;
+    }
+    this.annotations = this.ctrl.annotations || [];
+    this.buildFlotPairs(this.data);
+    const graphHeight = this.elem.height();
+    updateLegendValues(this.data, this.panel, graphHeight);
+
+    this.ctrl.events.emit('render-legend');
+  }
+
+  onGraphHover(evt) {
+    // ignore other graph hover events if shared tooltip is disabled
+    if (!this.dashboard.sharedTooltipModeEnabled()) {
+      return;
+    }
+
+    // ignore if we are the emitter
+    if (!this.plot || evt.panel.id === this.panel.id || this.ctrl.otherPanelInFullscreenMode()) {
+      return;
+    }
+
+    this.tooltip.show(evt.pos);
+  }
+
+  onPanelteardown() {
+    this.thresholdManager = null;
+
+    if (this.plot) {
+      this.plot.destroy();
+      this.plot = null;
+    }
+  }
+
+  onLegendRenderingComplete() {
+    this.render_panel();
+  }
+
+  onGraphHoverClear(event, info) {
+    if (this.plot) {
+      this.tooltip.clear(this.plot);
+    }
+  }
+
+  onPlotSelected(event, ranges) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      this.plot.clearSelection();
+      return;
+    }
+
+    if ((ranges.ctrlKey || ranges.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Add annotation
+      setTimeout(() => {
+        this.eventManager.updateTime(ranges.xaxis);
+      }, 100);
+    } else {
+      this.scope.$apply(() => {
+        this.timeSrv.setTime({
+          from: moment.utc(ranges.xaxis.from),
+          to: moment.utc(ranges.xaxis.to),
+        });
       });
-
-      // global events
-      appEvents.on(
-        'graph-hover',
-        evt => {
-          // ignore other graph hover events if shared tooltip is disabled
-          if (!dashboard.sharedTooltipModeEnabled()) {
-            return;
-          }
-
-          // ignore if we are the emitter
-          if (!plot || evt.panel.id === panel.id || ctrl.otherPanelInFullscreenMode()) {
-            return;
-          }
-
-          tooltip.show(evt.pos);
-        },
-        scope
-      );
-
-      appEvents.on(
-        'graph-hover-clear',
-        (event, info) => {
-          if (plot) {
-            tooltip.clear(plot);
-          }
-        },
-        scope
-      );
-
-      function shouldAbortRender() {
-        if (!data) {
-          return true;
-        }
-
-        if (panelWidth === 0) {
-          return true;
-        }
-
-        return false;
+    }
+  }
+
+  onPlotClick(event, pos, item) {
+    if (this.panel.xaxis.mode !== 'time') {
+      // Skip if panel in histogram or series mode
+      return;
+    }
+
+    if ((pos.ctrlKey || pos.metaKey) && (this.dashboard.meta.canEdit || this.dashboard.meta.canMakeEditable)) {
+      // Skip if range selected (added in "plotselected" event handler)
+      let isRangeSelection = pos.x !== pos.x1;
+      if (!isRangeSelection) {
+        setTimeout(() => {
+          this.eventManager.updateTime({ from: pos.x, to: null });
+        }, 100);
       }
-
-      function drawHook(plot) {
-        // add left axis labels
-        if (panel.yaxes[0].label && panel.yaxes[0].show) {
-          $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[0].label)
-            .appendTo(elem);
-        }
-
-        // add right axis labels
-        if (panel.yaxes[1].label && panel.yaxes[1].show) {
-          $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
-            .text(panel.yaxes[1].label)
-            .appendTo(elem);
-        }
-
-        if (ctrl.dataWarning) {
-          $(`<div class="datapoints-warning flot-temp-elem">${ctrl.dataWarning.title}</div>`).appendTo(elem);
-        }
-
-        thresholdManager.draw(plot);
+    }
+  }
+
+  onScopeDestroy() {
+    this.tooltip.destroy();
+    this.elem.off();
+    this.elem.remove();
+  }
+
+  shouldAbortRender() {
+    if (!this.data) {
+      return true;
+    }
+
+    if (this.panelWidth === 0) {
+      return true;
+    }
+
+    return false;
+  }
+
+  drawHook(plot) {
+    // add left axis labels
+    if (this.panel.yaxes[0].label && this.panel.yaxes[0].show) {
+      $("<div class='axisLabel left-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[0].label)
+        .appendTo(this.elem);
+    }
+
+    // add right axis labels
+    if (this.panel.yaxes[1].label && this.panel.yaxes[1].show) {
+      $("<div class='axisLabel right-yaxis-label flot-temp-elem'></div>")
+        .text(this.panel.yaxes[1].label)
+        .appendTo(this.elem);
+    }
+
+    if (this.ctrl.dataWarning) {
+      $(`<div class="datapoints-warning flot-temp-elem">${this.ctrl.dataWarning.title}</div>`).appendTo(this.elem);
+    }
+
+    this.thresholdManager.draw(plot);
+  }
+
+  processOffsetHook(plot, gridMargin) {
+    var left = this.panel.yaxes[0];
+    var right = this.panel.yaxes[1];
+    if (left.show && left.label) {
+      gridMargin.left = 20;
+    }
+    if (right.show && right.label) {
+      gridMargin.right = 20;
+    }
+
+    // apply y-axis min/max options
+    var yaxis = plot.getYAxes();
+    for (var i = 0; i < yaxis.length; i++) {
+      var axis = yaxis[i];
+      var panelOptions = this.panel.yaxes[i];
+      axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
+      axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
+    }
+  }
+
+  processRangeHook(plot) {
+    var yAxes = plot.getYAxes();
+    const align = this.panel.yaxis.align || false;
+
+    if (yAxes.length > 1 && align === true) {
+      const level = this.panel.yaxis.alignLevel || 0;
+      alignYLevel(yAxes, parseFloat(level));
+    }
+  }
+
+  // Series could have different timeSteps,
+  // let's find the smallest one so that bars are correctly rendered.
+  // In addition, only take series which are rendered as bars for this.
+  getMinTimeStepOfSeries(data) {
+    var min = Number.MAX_VALUE;
+
+    for (let i = 0; i < data.length; i++) {
+      if (!data[i].stats.timeStep) {
+        continue;
       }
-
-      function processOffsetHook(plot, gridMargin) {
-        var left = panel.yaxes[0];
-        var right = panel.yaxes[1];
-        if (left.show && left.label) {
-          gridMargin.left = 20;
-        }
-        if (right.show && right.label) {
-          gridMargin.right = 20;
+      if (this.panel.bars) {
+        if (data[i].bars && data[i].bars.show === false) {
+          continue;
         }
-
-        // apply y-axis min/max options
-        var yaxis = plot.getYAxes();
-        for (var i = 0; i < yaxis.length; i++) {
-          var axis = yaxis[i];
-          var panelOptions = panel.yaxes[i];
-          axis.options.max = axis.options.max !== null ? axis.options.max : panelOptions.max;
-          axis.options.min = axis.options.min !== null ? axis.options.min : panelOptions.min;
-        }
-      }
-
-      function processRangeHook(plot) {
-        var yAxes = plot.getYAxes();
-        const align = panel.yaxis.align || false;
-
-        if (yAxes.length > 1 && align === true) {
-          const level = panel.yaxis.alignLevel || 0;
-          alignYLevel(yAxes, parseFloat(level));
+      } else {
+        if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
+          continue;
         }
       }
 
-      // Series could have different timeSteps,
-      // let's find the smallest one so that bars are correctly rendered.
-      // In addition, only take series which are rendered as bars for this.
-      function getMinTimeStepOfSeries(data) {
-        var min = Number.MAX_VALUE;
-
-        for (let i = 0; i < data.length; i++) {
-          if (!data[i].stats.timeStep) {
-            continue;
-          }
-          if (panel.bars) {
-            if (data[i].bars && data[i].bars.show === false) {
-              continue;
-            }
-          } else {
-            if (typeof data[i].bars === 'undefined' || typeof data[i].bars.show === 'undefined' || !data[i].bars.show) {
-              continue;
-            }
-          }
-
-          if (data[i].stats.timeStep < min) {
-            min = data[i].stats.timeStep;
-          }
-        }
-
-        return min;
+      if (data[i].stats.timeStep < min) {
+        min = data[i].stats.timeStep;
       }
-
-      // Function for rendering panel
-      function render_panel() {
-        panelWidth = elem.width();
-        if (shouldAbortRender()) {
-          return;
-        }
-
-        // give space to alert editing
-        thresholdManager.prepare(elem, data);
-
-        // un-check dashes if lines are unchecked
-        panel.dashes = panel.lines ? panel.dashes : false;
-
-        // Populate element
-        let options: any = buildFlotOptions(panel);
-        prepareXAxis(options, panel);
-        configureYAxisOptions(data, options);
-        thresholdManager.addFlotOptions(options, panel);
-        eventManager.addFlotEvents(annotations, options);
-
-        sortedSeries = sortSeries(data, panel);
-        callPlot(options, true);
+    }
+
+    return min;
+  }
+
+  // Function for rendering panel
+  render_panel() {
+    this.panelWidth = this.elem.width();
+    if (this.shouldAbortRender()) {
+      return;
+    }
+
+    // give space to alert editing
+    this.thresholdManager.prepare(this.elem, this.data);
+
+    // un-check dashes if lines are unchecked
+    this.panel.dashes = this.panel.lines ? this.panel.dashes : false;
+
+    // Populate element
+    let options: any = this.buildFlotOptions(this.panel);
+    this.prepareXAxis(options, this.panel);
+    this.configureYAxisOptions(this.data, options);
+    this.thresholdManager.addFlotOptions(options, this.panel);
+    this.eventManager.addFlotEvents(this.annotations, options);
+
+    this.sortedSeries = this.sortSeries(this.data, this.panel);
+    this.callPlot(options, true);
+  }
+
+  buildFlotPairs(data) {
+    for (let i = 0; i < data.length; i++) {
+      let series = data[i];
+      series.data = series.getFlotPairs(series.nullPointMode || this.panel.nullPointMode);
+
+      // if hidden remove points and disable stack
+      if (this.ctrl.hiddenSeries[series.alias]) {
+        series.data = [];
+        series.stack = false;
       }
+    }
+  }
 
-      function buildFlotPairs(data) {
-        for (let i = 0; i < data.length; i++) {
-          let series = data[i];
-          series.data = series.getFlotPairs(series.nullPointMode || panel.nullPointMode);
+  prepareXAxis(options, panel) {
+    switch (panel.xaxis.mode) {
+      case 'series': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
 
-          // if hidden remove points and disable stack
-          if (ctrl.hiddenSeries[series.alias]) {
-            series.data = [];
-            series.stack = false;
-          }
+        for (let i = 0; i < this.data.length; i++) {
+          let series = this.data[i];
+          series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
         }
-      }
 
-      function prepareXAxis(options, panel) {
-        switch (panel.xaxis.mode) {
-          case 'series': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-
-            for (let i = 0; i < data.length; i++) {
-              let series = data[i];
-              series.data = [[i + 1, series.stats[panel.xaxis.values[0]]]];
-            }
-
-            addXSeriesAxis(options);
-            break;
-          }
-          case 'histogram': {
-            let bucketSize: number;
-
-            if (data.length) {
-              let histMin = _.min(_.map(data, s => s.stats.min));
-              let histMax = _.max(_.map(data, s => s.stats.max));
-              let ticks = panel.xaxis.buckets || panelWidth / 50;
-              bucketSize = tickStep(histMin, histMax, ticks);
-              options.series.bars.barWidth = bucketSize * 0.8;
-              data = convertToHistogramData(data, bucketSize, ctrl.hiddenSeries, histMin, histMax);
-            } else {
-              bucketSize = 0;
-            }
-
-            addXHistogramAxis(options, bucketSize);
-            break;
-          }
-          case 'table': {
-            options.series.bars.barWidth = 0.7;
-            options.series.bars.align = 'center';
-            addXTableAxis(options);
-            break;
-          }
-          default: {
-            options.series.bars.barWidth = getMinTimeStepOfSeries(data) / 1.5;
-            addTimeAxis(options);
-            break;
-          }
-        }
+        this.addXSeriesAxis(options);
+        break;
       }
-
-      function callPlot(options, incrementRenderCounter) {
-        try {
-          plot = $.plot(elem, sortedSeries, options);
-          if (ctrl.renderError) {
-            delete ctrl.error;
-            delete ctrl.inspector;
-          }
-        } catch (e) {
-          console.log('flotcharts error', e);
-          ctrl.error = e.message || 'Render Error';
-          ctrl.renderError = true;
-          ctrl.inspector = { error: e };
+      case 'histogram': {
+        let bucketSize: number;
+
+        if (this.data.length) {
+          let histMin = _.min(_.map(this.data, s => s.stats.min));
+          let histMax = _.max(_.map(this.data, s => s.stats.max));
+          let ticks = panel.xaxis.buckets || this.panelWidth / 50;
+          bucketSize = tickStep(histMin, histMax, ticks);
+          options.series.bars.barWidth = bucketSize * 0.8;
+          this.data = convertToHistogramData(this.data, bucketSize, this.ctrl.hiddenSeries, histMin, histMax);
+        } else {
+          bucketSize = 0;
         }
 
-        if (incrementRenderCounter) {
-          ctrl.renderingCompleted();
-        }
+        this.addXHistogramAxis(options, bucketSize);
+        break;
       }
-
-      function buildFlotOptions(panel) {
-        let gridColor = '#c8c8c8';
-        if (config.bootData.user.lightTheme === true) {
-          gridColor = '#a1a1a1';
-        }
-        const stack = panel.stack ? true : null;
-        let options = {
-          hooks: {
-            draw: [drawHook],
-            processOffset: [processOffsetHook],
-            processRange: [processRangeHook],
-          },
-          legend: { show: false },
-          series: {
-            stackpercent: panel.stack ? panel.percentage : false,
-            stack: panel.percentage ? null : stack,
-            lines: {
-              show: panel.lines,
-              zero: false,
-              fill: translateFillOption(panel.fill),
-              lineWidth: panel.dashes ? 0 : panel.linewidth,
-              steps: panel.steppedLine,
-            },
-            dashes: {
-              show: panel.dashes,
-              lineWidth: panel.linewidth,
-              dashLength: [panel.dashLength, panel.spaceLength],
-            },
-            bars: {
-              show: panel.bars,
-              fill: 1,
-              barWidth: 1,
-              zero: false,
-              lineWidth: 0,
-            },
-            points: {
-              show: panel.points,
-              fill: 1,
-              fillColor: false,
-              radius: panel.points ? panel.pointradius : 2,
-            },
-            shadowSize: 0,
-          },
-          yaxes: [],
-          xaxis: {},
-          grid: {
-            minBorderMargin: 0,
-            markings: [],
-            backgroundColor: null,
-            borderWidth: 0,
-            hoverable: true,
-            clickable: true,
-            color: gridColor,
-            margin: { left: 0, right: 0 },
-            labelMarginX: 0,
-          },
-          selection: {
-            mode: 'x',
-            color: '#666',
-          },
-          crosshair: {
-            mode: 'x',
-          },
-        };
-        return options;
+      case 'table': {
+        options.series.bars.barWidth = 0.7;
+        options.series.bars.align = 'center';
+        this.addXTableAxis(options);
+        break;
       }
-
-      function sortSeries(series, panel) {
-        var sortBy = panel.legend.sort;
-        var sortOrder = panel.legend.sortDesc;
-        var haveSortBy = sortBy !== null && sortBy !== undefined;
-        var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
-        var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
-        var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
-
-        if (shouldSortBy) {
-          return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
-        } else {
-          return _.sortBy(series, s => s.zindex);
-        }
+      default: {
+        options.series.bars.barWidth = this.getMinTimeStepOfSeries(this.data) / 1.5;
+        this.addTimeAxis(options);
+        break;
       }
-
-      function translateFillOption(fill) {
-        if (panel.percentage && panel.stack) {
-          return fill === 0 ? 0.001 : fill / 10;
-        } else {
-          return fill / 10;
+    }
+  }
+
+  callPlot(options, incrementRenderCounter) {
+    try {
+      this.plot = $.plot(this.elem, this.sortedSeries, options);
+      if (this.ctrl.renderError) {
+        delete this.ctrl.error;
+        delete this.ctrl.inspector;
+      }
+    } catch (e) {
+      console.log('flotcharts error', e);
+      this.ctrl.error = e.message || 'Render Error';
+      this.ctrl.renderError = true;
+      this.ctrl.inspector = { error: e };
+    }
+
+    if (incrementRenderCounter) {
+      this.ctrl.renderingCompleted();
+    }
+  }
+
+  buildFlotOptions(panel) {
+    let gridColor = '#c8c8c8';
+    if (config.bootData.user.lightTheme === true) {
+      gridColor = '#a1a1a1';
+    }
+    const stack = panel.stack ? true : null;
+    let options = {
+      hooks: {
+        draw: [this.drawHook.bind(this)],
+        processOffset: [this.processOffsetHook.bind(this)],
+        processRange: [this.processRangeHook.bind(this)],
+      },
+      legend: { show: false },
+      series: {
+        stackpercent: panel.stack ? panel.percentage : false,
+        stack: panel.percentage ? null : stack,
+        lines: {
+          show: panel.lines,
+          zero: false,
+          fill: this.translateFillOption(panel.fill),
+          lineWidth: panel.dashes ? 0 : panel.linewidth,
+          steps: panel.steppedLine,
+        },
+        dashes: {
+          show: panel.dashes,
+          lineWidth: panel.linewidth,
+          dashLength: [panel.dashLength, panel.spaceLength],
+        },
+        bars: {
+          show: panel.bars,
+          fill: 1,
+          barWidth: 1,
+          zero: false,
+          lineWidth: 0,
+        },
+        points: {
+          show: panel.points,
+          fill: 1,
+          fillColor: false,
+          radius: panel.points ? panel.pointradius : 2,
+        },
+        shadowSize: 0,
+      },
+      yaxes: [],
+      xaxis: {},
+      grid: {
+        minBorderMargin: 0,
+        markings: [],
+        backgroundColor: null,
+        borderWidth: 0,
+        hoverable: true,
+        clickable: true,
+        color: gridColor,
+        margin: { left: 0, right: 0 },
+        labelMarginX: 0,
+      },
+      selection: {
+        mode: 'x',
+        color: '#666',
+      },
+      crosshair: {
+        mode: 'x',
+      },
+    };
+    return options;
+  }
+
+  sortSeries(series, panel) {
+    var sortBy = panel.legend.sort;
+    var sortOrder = panel.legend.sortDesc;
+    var haveSortBy = sortBy !== null && sortBy !== undefined;
+    var haveSortOrder = sortOrder !== null && sortOrder !== undefined;
+    var shouldSortBy = panel.stack && haveSortBy && haveSortOrder;
+    var sortDesc = panel.legend.sortDesc === true ? -1 : 1;
+
+    if (shouldSortBy) {
+      return _.sortBy(series, s => s.stats[sortBy] * sortDesc);
+    } else {
+      return _.sortBy(series, s => s.zindex);
+    }
+  }
+
+  translateFillOption(fill) {
+    if (this.panel.percentage && this.panel.stack) {
+      return fill === 0 ? 0.001 : fill / 10;
+    } else {
+      return fill / 10;
+    }
+  }
+
+  addTimeAxis(options) {
+    var ticks = this.panelWidth / 100;
+    var min = _.isUndefined(this.ctrl.range.from) ? null : this.ctrl.range.from.valueOf();
+    var max = _.isUndefined(this.ctrl.range.to) ? null : this.ctrl.range.to.valueOf();
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: 'time',
+      min: min,
+      max: max,
+      label: 'Datetime',
+      ticks: ticks,
+      timeformat: this.time_format(ticks, min, max),
+    };
+  }
+
+  addXSeriesAxis(options) {
+    var ticks = _.map(this.data, function(series, index) {
+      return [index + 1, series.alias];
+    });
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  addXHistogramAxis(options, bucketSize) {
+    let ticks, min, max;
+    let defaultTicks = this.panelWidth / 50;
+
+    if (this.data.length && bucketSize) {
+      let tick_values = [];
+      for (let d of this.data) {
+        for (let point of d.data) {
+          tick_values[point[0]] = true;
         }
       }
-
-      function addTimeAxis(options) {
-        var ticks = panelWidth / 100;
-        var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf();
-        var max = _.isUndefined(ctrl.range.to) ? null : ctrl.range.to.valueOf();
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: 'time',
-          min: min,
-          max: max,
-          label: 'Datetime',
-          ticks: ticks,
-          timeformat: time_format(ticks, min, max),
-        };
+      ticks = Object.keys(tick_values).map(v => Number(v));
+      min = _.min(ticks);
+      max = _.max(ticks);
+
+      // Adjust tick step
+      let tickStep = bucketSize;
+      let ticks_num = Math.floor((max - min) / tickStep);
+      while (ticks_num > defaultTicks) {
+        tickStep = tickStep * 2;
+        ticks_num = Math.ceil((max - min) / tickStep);
       }
 
-      function addXSeriesAxis(options) {
-        var ticks = _.map(data, function(series, index) {
-          return [index + 1, series.alias];
-        });
+      // Expand ticks for pretty view
+      min = Math.floor(min / tickStep) * tickStep;
+      // 1.01 is 101% - ensure we have enough space for last bar
+      max = Math.ceil(max * 1.01 / tickStep) * tickStep;
 
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
+      ticks = [];
+      for (let i = min; i <= max; i += tickStep) {
+        ticks.push(i);
       }
-
-      function addXHistogramAxis(options, bucketSize) {
-        let ticks, min, max;
-        let defaultTicks = panelWidth / 50;
-
-        if (data.length && bucketSize) {
-          let tick_values = [];
-          for (let d of data) {
-            for (let point of d.data) {
-              tick_values[point[0]] = true;
-            }
-          }
-          ticks = Object.keys(tick_values).map(v => Number(v));
-          min = _.min(ticks);
-          max = _.max(ticks);
-
-          // Adjust tick step
-          let tickStep = bucketSize;
-          let ticks_num = Math.floor((max - min) / tickStep);
-          while (ticks_num > defaultTicks) {
-            tickStep = tickStep * 2;
-            ticks_num = Math.ceil((max - min) / tickStep);
-          }
-
-          // Expand ticks for pretty view
-          min = Math.floor(min / tickStep) * tickStep;
-          // 1.01 is 101% - ensure we have enough space for last bar
-          max = Math.ceil(max * 1.01 / tickStep) * tickStep;
-
-          ticks = [];
-          for (let i = min; i <= max; i += tickStep) {
-            ticks.push(i);
-          }
-        } else {
-          // Set defaults if no data
-          ticks = defaultTicks / 2;
-          min = 0;
-          max = 1;
+    } else {
+      // Set defaults if no data
+      ticks = defaultTicks / 2;
+      min = 0;
+      max = 1;
+    }
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: min,
+      max: max,
+      label: 'Histogram',
+      ticks: ticks,
+    };
+
+    // Use 'short' format for histogram values
+    this.configureAxisMode(options.xaxis, 'short');
+  }
+
+  addXTableAxis(options) {
+    var ticks = _.map(this.data, function(series, seriesIndex) {
+      return _.map(series.datapoints, function(point, pointIndex) {
+        var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
+        return [tickIndex + 1, point[1]];
+      });
+    });
+    ticks = _.flatten(ticks, true);
+
+    options.xaxis = {
+      timezone: this.dashboard.getTimezone(),
+      show: this.panel.xaxis.show,
+      mode: null,
+      min: 0,
+      max: ticks.length + 1,
+      label: 'Datetime',
+      ticks: ticks,
+    };
+  }
+
+  configureYAxisOptions(data, options) {
+    var defaults = {
+      position: 'left',
+      show: this.panel.yaxes[0].show,
+      index: 1,
+      logBase: this.panel.yaxes[0].logBase || 1,
+      min: this.parseNumber(this.panel.yaxes[0].min),
+      max: this.parseNumber(this.panel.yaxes[0].max),
+      tickDecimals: this.panel.yaxes[0].decimals,
+    };
+
+    options.yaxes.push(defaults);
+
+    if (_.find(data, { yaxis: 2 })) {
+      var secondY = _.clone(defaults);
+      secondY.index = 2;
+      secondY.show = this.panel.yaxes[1].show;
+      secondY.logBase = this.panel.yaxes[1].logBase || 1;
+      secondY.position = 'right';
+      secondY.min = this.parseNumber(this.panel.yaxes[1].min);
+      secondY.max = this.parseNumber(this.panel.yaxes[1].max);
+      secondY.tickDecimals = this.panel.yaxes[1].decimals;
+      options.yaxes.push(secondY);
+
+      this.applyLogScale(options.yaxes[1], data);
+      this.configureAxisMode(
+        options.yaxes[1],
+        this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[1].format
+      );
+    }
+    this.applyLogScale(options.yaxes[0], data);
+    this.configureAxisMode(
+      options.yaxes[0],
+      this.panel.percentage && this.panel.stack ? 'percent' : this.panel.yaxes[0].format
+    );
+  }
+
+  parseNumber(value: any) {
+    if (value === null || typeof value === 'undefined') {
+      return null;
+    }
+
+    return _.toNumber(value);
+  }
+
+  applyLogScale(axis, data) {
+    if (axis.logBase === 1) {
+      return;
+    }
+
+    const minSetToZero = axis.min === 0;
+
+    if (axis.min < Number.MIN_VALUE) {
+      axis.min = null;
+    }
+    if (axis.max < Number.MIN_VALUE) {
+      axis.max = null;
+    }
+
+    var series, i;
+    var max = axis.max,
+      min = axis.min;
+
+    for (i = 0; i < data.length; i++) {
+      series = data[i];
+      if (series.yaxis === axis.index) {
+        if (!max || max < series.stats.max) {
+          max = series.stats.max;
+        }
+        if (!min || min > series.stats.logmin) {
+          min = series.stats.logmin;
         }
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: min,
-          max: max,
-          label: 'Histogram',
-          ticks: ticks,
-        };
-
-        // Use 'short' format for histogram values
-        configureAxisMode(options.xaxis, 'short');
       }
-
-      function addXTableAxis(options) {
-        var ticks = _.map(data, function(series, seriesIndex) {
-          return _.map(series.datapoints, function(point, pointIndex) {
-            var tickIndex = seriesIndex * series.datapoints.length + pointIndex;
-            return [tickIndex + 1, point[1]];
-          });
-        });
-        ticks = _.flatten(ticks, true);
-
-        options.xaxis = {
-          timezone: dashboard.getTimezone(),
-          show: panel.xaxis.show,
-          mode: null,
-          min: 0,
-          max: ticks.length + 1,
-          label: 'Datetime',
-          ticks: ticks,
-        };
+    }
+
+    axis.transform = function(v) {
+      return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
+    };
+    axis.inverseTransform = function(v) {
+      return Math.pow(axis.logBase, v);
+    };
+
+    if (!max && !min) {
+      max = axis.inverseTransform(+2);
+      min = axis.inverseTransform(-2);
+    } else if (!max) {
+      max = min * axis.inverseTransform(+4);
+    } else if (!min) {
+      min = max * axis.inverseTransform(-4);
+    }
+
+    if (axis.min) {
+      min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
+    } else {
+      min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
+    }
+    if (axis.max) {
+      max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
+    } else {
+      max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
+    }
+
+    if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
+      return;
+    }
+
+    if (Number.isFinite(min) && Number.isFinite(max)) {
+      if (minSetToZero) {
+        axis.min = 0.1;
+        min = 1;
       }
 
-      function configureYAxisOptions(data, options) {
-        var defaults = {
-          position: 'left',
-          show: panel.yaxes[0].show,
-          index: 1,
-          logBase: panel.yaxes[0].logBase || 1,
-          min: parseNumber(panel.yaxes[0].min),
-          max: parseNumber(panel.yaxes[0].max),
-          tickDecimals: panel.yaxes[0].decimals,
-        };
-
-        options.yaxes.push(defaults);
-
-        if (_.find(data, { yaxis: 2 })) {
-          var secondY = _.clone(defaults);
-          secondY.index = 2;
-          secondY.show = panel.yaxes[1].show;
-          secondY.logBase = panel.yaxes[1].logBase || 1;
-          secondY.position = 'right';
-          secondY.min = parseNumber(panel.yaxes[1].min);
-          secondY.max = parseNumber(panel.yaxes[1].max);
-          secondY.tickDecimals = panel.yaxes[1].decimals;
-          options.yaxes.push(secondY);
-
-          applyLogScale(options.yaxes[1], data);
-          configureAxisMode(options.yaxes[1], panel.percentage && panel.stack ? 'percent' : panel.yaxes[1].format);
-        }
-        applyLogScale(options.yaxes[0], data);
-        configureAxisMode(options.yaxes[0], panel.percentage && panel.stack ? 'percent' : panel.yaxes[0].format);
+      axis.ticks = this.generateTicksForLogScaleYAxis(min, max, axis.logBase);
+      if (minSetToZero) {
+        axis.ticks.unshift(0.1);
       }
-
-      function parseNumber(value: any) {
-        if (value === null || typeof value === 'undefined') {
-          return null;
-        }
-
-        return _.toNumber(value);
+      if (axis.ticks[axis.ticks.length - 1] > axis.max) {
+        axis.max = axis.ticks[axis.ticks.length - 1];
       }
-
-      function applyLogScale(axis, data) {
-        if (axis.logBase === 1) {
-          return;
-        }
-
-        const minSetToZero = axis.min === 0;
-
-        if (axis.min < Number.MIN_VALUE) {
-          axis.min = null;
-        }
-        if (axis.max < Number.MIN_VALUE) {
-          axis.max = null;
-        }
-
-        var series, i;
-        var max = axis.max,
-          min = axis.min;
-
-        for (i = 0; i < data.length; i++) {
-          series = data[i];
-          if (series.yaxis === axis.index) {
-            if (!max || max < series.stats.max) {
-              max = series.stats.max;
-            }
-            if (!min || min > series.stats.logmin) {
-              min = series.stats.logmin;
-            }
-          }
-        }
-
-        axis.transform = function(v) {
-          return v < Number.MIN_VALUE ? null : Math.log(v) / Math.log(axis.logBase);
-        };
-        axis.inverseTransform = function(v) {
-          return Math.pow(axis.logBase, v);
-        };
-
-        if (!max && !min) {
-          max = axis.inverseTransform(+2);
-          min = axis.inverseTransform(-2);
-        } else if (!max) {
-          max = min * axis.inverseTransform(+4);
-        } else if (!min) {
-          min = max * axis.inverseTransform(-4);
-        }
-
-        if (axis.min) {
-          min = axis.inverseTransform(Math.ceil(axis.transform(axis.min)));
-        } else {
-          min = axis.min = axis.inverseTransform(Math.floor(axis.transform(min)));
-        }
-        if (axis.max) {
-          max = axis.inverseTransform(Math.floor(axis.transform(axis.max)));
-        } else {
-          max = axis.max = axis.inverseTransform(Math.ceil(axis.transform(max)));
-        }
-
-        if (!min || min < Number.MIN_VALUE || !max || max < Number.MIN_VALUE) {
-          return;
-        }
-
-        if (Number.isFinite(min) && Number.isFinite(max)) {
-          if (minSetToZero) {
-            axis.min = 0.1;
-            min = 1;
-          }
-
-          axis.ticks = generateTicksForLogScaleYAxis(min, max, axis.logBase);
-          if (minSetToZero) {
-            axis.ticks.unshift(0.1);
-          }
-          if (axis.ticks[axis.ticks.length - 1] > axis.max) {
-            axis.max = axis.ticks[axis.ticks.length - 1];
-          }
-        } else {
-          axis.ticks = [1, 2];
-          delete axis.min;
-          delete axis.max;
-        }
+    } else {
+      axis.ticks = [1, 2];
+      delete axis.min;
+      delete axis.max;
+    }
+  }
+
+  generateTicksForLogScaleYAxis(min, max, logBase) {
+    let ticks = [];
+
+    var nextTick;
+    for (nextTick = min; nextTick <= max; nextTick *= logBase) {
+      ticks.push(nextTick);
+    }
+
+    const maxNumTicks = Math.ceil(this.ctrl.height / 25);
+    const numTicks = ticks.length;
+    if (numTicks > maxNumTicks) {
+      const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
+      ticks = [];
+
+      for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
+        ticks.push(nextTick);
       }
+    }
 
-      function generateTicksForLogScaleYAxis(min, max, logBase) {
-        let ticks = [];
-
-        var nextTick;
-        for (nextTick = min; nextTick <= max; nextTick *= logBase) {
-          ticks.push(nextTick);
-        }
-
-        const maxNumTicks = Math.ceil(ctrl.height / 25);
-        const numTicks = ticks.length;
-        if (numTicks > maxNumTicks) {
-          const factor = Math.ceil(numTicks / maxNumTicks) * logBase;
-          ticks = [];
-
-          for (nextTick = min; nextTick <= max * factor; nextTick *= factor) {
-            ticks.push(nextTick);
-          }
-        }
+    return ticks;
+  }
 
-        return ticks;
+  configureAxisMode(axis, format) {
+    axis.tickFormatter = function(val, axis) {
+      if (!kbn.valueFormats[format]) {
+        throw new Error(`Unit '${format}' is not supported`);
       }
-
-      function configureAxisMode(axis, format) {
-        axis.tickFormatter = function(val, axis) {
-          if (!kbn.valueFormats[format]) {
-            throw new Error(`Unit '${format}' is not supported`);
-          }
-          return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
-        };
+      return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals);
+    };
+  }
+
+  time_format(ticks, min, max) {
+    if (min && max && ticks) {
+      var range = max - min;
+      var secPerTick = range / ticks / 1000;
+      var oneDay = 86400000;
+      var oneYear = 31536000000;
+
+      if (secPerTick <= 45) {
+        return '%H:%M:%S';
       }
-
-      function time_format(ticks, min, max) {
-        if (min && max && ticks) {
-          var range = max - min;
-          var secPerTick = range / ticks / 1000;
-          var oneDay = 86400000;
-          var oneYear = 31536000000;
-
-          if (secPerTick <= 45) {
-            return '%H:%M:%S';
-          }
-          if (secPerTick <= 7200 || range <= oneDay) {
-            return '%H:%M';
-          }
-          if (secPerTick <= 80000) {
-            return '%m/%d %H:%M';
-          }
-          if (secPerTick <= 2419200 || range <= oneYear) {
-            return '%m/%d';
-          }
-          return '%Y-%m';
-        }
-
+      if (secPerTick <= 7200 || range <= oneDay) {
         return '%H:%M';
       }
+      if (secPerTick <= 80000) {
+        return '%m/%d %H:%M';
+      }
+      if (secPerTick <= 2419200 || range <= oneYear) {
+        return '%m/%d';
+      }
+      return '%Y-%m';
+    }
 
-      elem.bind('plotselected', function(event, ranges) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          plot.clearSelection();
-          return;
-        }
-
-        if ((ranges.ctrlKey || ranges.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Add annotation
-          setTimeout(() => {
-            eventManager.updateTime(ranges.xaxis);
-          }, 100);
-        } else {
-          scope.$apply(function() {
-            timeSrv.setTime({
-              from: moment.utc(ranges.xaxis.from),
-              to: moment.utc(ranges.xaxis.to),
-            });
-          });
-        }
-      });
-
-      elem.bind('plotclick', function(event, pos, item) {
-        if (panel.xaxis.mode !== 'time') {
-          // Skip if panel in histogram or series mode
-          return;
-        }
-
-        if ((pos.ctrlKey || pos.metaKey) && (dashboard.meta.canEdit || dashboard.meta.canMakeEditable)) {
-          // Skip if range selected (added in "plotselected" event handler)
-          let isRangeSelection = pos.x !== pos.x1;
-          if (!isRangeSelection) {
-            setTimeout(() => {
-              eventManager.updateTime({ from: pos.x, to: null });
-            }, 100);
-          }
-        }
-      });
+    return '%H:%M';
+  }
+}
 
-      scope.$on('$destroy', function() {
-        tooltip.destroy();
-        elem.off();
-        elem.remove();
-      });
+/** @ngInject **/
+function graphDirective(timeSrv, popoverSrv, contextSrv) {
+  return {
+    restrict: 'A',
+    template: '',
+    link: (scope, elem) => {
+      return new GraphElement(scope, elem, timeSrv);
     },
   };
 }
 
 coreModule.directive('grafanaGraph', graphDirective);
+export { GraphElement, graphDirective };

+ 1 - 0
public/app/plugins/panel/graph/module.ts

@@ -13,6 +13,7 @@ import { axesEditorComponent } from './axes_editor';
 class GraphCtrl extends MetricsPanelCtrl {
   static template = template;
 
+  renderError: boolean;
   hiddenSeries: any = {};
   seriesList: any = [];
   dataList: any = [];

+ 518 - 0
public/app/plugins/panel/graph/specs/graph.jest.ts

@@ -0,0 +1,518 @@
+jest.mock('app/features/annotations/all', () => ({
+  EventManager: function() {
+    return {
+      on: () => {},
+      addFlotEvents: () => {},
+    };
+  },
+}));
+
+jest.mock('app/core/core', () => ({
+  coreModule: {
+    directive: () => {},
+  },
+  appEvents: {
+    on: () => {},
+  },
+}));
+
+import '../module';
+import { GraphCtrl } from '../module';
+import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl';
+import { PanelCtrl } from 'app/features/panel/panel_ctrl';
+
+import config from 'app/core/config';
+
+import TimeSeries from 'app/core/time_series2';
+import moment from 'moment';
+import $ from 'jquery';
+import { graphDirective } from '../graph';
+
+let ctx = <any>{};
+let ctrl;
+let scope = {
+  ctrl: {},
+  range: {
+    from: moment([2015, 1, 1]),
+    to: moment([2015, 11, 20]),
+  },
+  $on: () => {},
+};
+let link;
+
+describe('grafanaGraph', function() {
+  const setupCtx = (beforeRender?) => {
+    config.bootData = {
+      user: {
+        lightTheme: false,
+      },
+    };
+    GraphCtrl.prototype = <any>{
+      ...MetricsPanelCtrl.prototype,
+      ...PanelCtrl.prototype,
+      ...GraphCtrl.prototype,
+      height: 200,
+      panel: {
+        events: {
+          on: () => {},
+        },
+        legend: {},
+        grid: {},
+        yaxes: [
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+          {
+            min: null,
+            max: null,
+            format: 'short',
+            logBase: 1,
+          },
+        ],
+        thresholds: [],
+        xaxis: {},
+        seriesOverrides: [],
+        tooltip: {
+          shared: true,
+        },
+      },
+      renderingCompleted: jest.fn(),
+      hiddenSeries: {},
+      dashboard: {
+        getTimezone: () => 'browser',
+      },
+      range: {
+        from: moment([2015, 1, 1, 10]),
+        to: moment([2015, 1, 1, 22]),
+      },
+    };
+
+    ctx.data = [];
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[1, 1], [2, 2]],
+        alias: 'series1',
+      })
+    );
+    ctx.data.push(
+      new TimeSeries({
+        datapoints: [[10, 1], [20, 2]],
+        alias: 'series2',
+      })
+    );
+
+    ctrl = new GraphCtrl(
+      {
+        $on: () => {},
+      },
+      {
+        get: () => {},
+      },
+      {}
+    );
+
+    $.plot = ctrl.plot = jest.fn();
+    scope.ctrl = ctrl;
+
+    link = graphDirective({}, {}, {}).link(scope, { width: () => 500, mouseleave: () => {}, bind: () => {} });
+    if (typeof beforeRender === 'function') {
+      beforeRender();
+    }
+    link.data = ctx.data;
+
+    //Emulate functions called by event listeners
+    link.buildFlotPairs(link.data);
+    link.render_panel();
+    ctx.plotData = ctrl.plot.mock.calls[0][1];
+
+    ctx.plotOptions = ctrl.plot.mock.calls[0][2];
+  };
+
+  describe('simple lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctrl.panel.linewidth = 3;
+        ctrl.panel.steppedLine = true;
+      });
+    });
+
+    it('should configure plot with correct options', () => {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotOptions.series.lines.lineWidth).toBe(3);
+      expect(ctx.plotOptions.series.lines.steps).toBe(true);
+    });
+  });
+
+  describe('sorting stacked series as legend. disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = undefined;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('should not modify order of time series', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. min descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('sorting stacked series as legend. min ascending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = false;
+        ctrl.panel.stack = true;
+      });
+    });
+    it('lowest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. stacking disabled', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = false;
+      });
+    });
+
+    it('highest value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].alias).toBe('series2');
+    });
+  });
+
+  describe('sorting stacked series as legend. current descending order', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.legend.sort = 'current';
+        ctrl.panel.legend.sortDesc = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('highest last value should be first', () => {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when logBase is log 10', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
+          alias: 'seriesFixedscale',
+        });
+        ctx.data[1].yaxis = 2;
+        ctrl.panel.yaxes[0].logBase = 10;
+
+        ctrl.panel.yaxes[1].logBase = 10;
+        ctrl.panel.yaxes[1].min = '0.05';
+        ctrl.panel.yaxes[1].max = '1500';
+      });
+    });
+
+    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBeCloseTo(0.001);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBeCloseTo(8);
+      expect(axisAutoscale.ticks[0]).toBeCloseTo(0.001);
+      if (axisAutoscale.ticks.length === 7) {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBeCloseTo(1000);
+      } else {
+        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).toBe(10000);
+      }
+
+      var axisFixedscale = ctx.plotOptions.yaxes[1];
+      expect(axisFixedscale.min).toBe(0.05);
+      expect(axisFixedscale.max).toBe(1500);
+      expect(axisFixedscale.ticks.length).toBe(5);
+      expect(axisFixedscale.ticks[0]).toBe(0.1);
+      expect(axisFixedscale.ticks[4]).toBe(1000);
+    });
+  });
+
+  describe('when logBase is log 10 and data points contain only zeroes', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should not set min and max and should create some fake ticks', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(undefined);
+      expect(axisAutoscale.max).toBe(undefined);
+      expect(axisAutoscale.ticks.length).toBe(2);
+      expect(axisAutoscale.ticks[0]).toBe(1);
+      expect(axisAutoscale.ticks[1]).toBe(2);
+    });
+  });
+
+  // y-min set 0 is a special case for log scale,
+  // this approximates it by setting min to 0.1
+  describe('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.yaxes[0].logBase = 10;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+    it('should set min to 0.1 and add a tick for 0.1', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.transform(100)).toBe(2);
+      expect(axisAutoscale.inverseTransform(-3)).toBeCloseTo(0.001);
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.max).toBe(10000);
+      expect(axisAutoscale.ticks.length).toBe(6);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[5]).toBe(10000);
+    });
+  });
+
+  describe('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        const heightForApprox5Ticks = 125;
+        ctrl.height = heightForApprox5Ticks;
+        ctrl.panel.yaxes[0].logBase = 2;
+        ctrl.panel.yaxes[0].min = '0';
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
+          alias: 'seriesAutoscale',
+        });
+        ctx.data[0].yaxis = 1;
+      });
+    });
+
+    it('should regenerate ticks so that if fits on the y-axis', function() {
+      var axisAutoscale = ctx.plotOptions.yaxes[0];
+      expect(axisAutoscale.min).toBe(0.1);
+      expect(axisAutoscale.ticks.length).toBe(8);
+      expect(axisAutoscale.ticks[0]).toBe(0.1);
+      expect(axisAutoscale.ticks[7]).toBe(262144);
+      expect(axisAutoscale.max).toBe(262144);
+    });
+
+    it('should set axis max to be max tick value', function() {
+      expect(ctx.plotOptions.yaxes[0].max).toBe(262144);
+    });
+  });
+
+  describe('dashed lines options', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.linewidth = 2;
+        ctrl.panel.dashes = true;
+      });
+    });
+
+    it('should configure dashed plot with correct options', function() {
+      expect(ctx.plotOptions.series.lines.show).toBe(true);
+      expect(ctx.plotOptions.series.dashes.lineWidth).toBe(2);
+      expect(ctx.plotOptions.series.dashes.show).toBe(true);
+    });
+  });
+
+  describe('should use timeStep for barWidth', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.bars = true;
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[1, 10], [2, 20]],
+          alias: 'series1',
+        });
+      });
+    });
+
+    it('should set barWidth', function() {
+      expect(ctx.plotOptions.series.bars.barWidth).toBe(1 / 1.5);
+    });
+  });
+
+  describe('series option overrides, fill & points', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.lines = true;
+        ctrl.panel.fill = 5;
+        ctx.data[0].zindex = 10;
+        ctx.data[1].alias = 'test';
+        ctx.data[1].lines = { fill: 0.001 };
+        ctx.data[1].points = { show: true };
+      });
+    });
+
+    it('should match second series and fill zero, and enable points', function() {
+      expect(ctx.plotOptions.series.lines.fill).toBe(0.5);
+      expect(ctx.plotData[1].lines.fill).toBe(0.001);
+      expect(ctx.plotData[1].points.show).toBe(true);
+    });
+  });
+
+  describe('should order series order according to zindex', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctx.data[1].zindex = 1;
+        ctx.data[0].zindex = 10;
+      });
+    });
+
+    it('should move zindex 2 last', function() {
+      expect(ctx.plotData[0].alias).toBe('series2');
+      expect(ctx.plotData[1].alias).toBe('series1');
+    });
+  });
+
+  describe('when series is hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.hiddenSeries = { series2: true };
+      });
+    });
+
+    it('should remove datapoints and disable stack', function() {
+      expect(ctx.plotData[0].alias).toBe('series1');
+      expect(ctx.plotData[1].data.length).toBe(0);
+      expect(ctx.plotData[1].stack).toBe(false);
+    });
+  });
+
+  describe('when stack and percent', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.percentage = true;
+        ctrl.panel.stack = true;
+      });
+    });
+
+    it('should show percentage', function() {
+      var axis = ctx.plotOptions.yaxes[0];
+      expect(axis.tickFormatter(100, axis)).toBe('100%');
+    });
+  });
+
+  describe('when panel too narrow to show x-axis dates in same granularity as wide panels', () => {
+    //Set width to 10px
+    describe('and the range is less than 24 hours', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1, 10]);
+          ctrl.range.to = moment([2015, 1, 1, 22]);
+        });
+      });
+
+      it('should format dates as hours minutes', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%H:%M');
+      });
+    });
+
+    describe('and the range is less than one year', function() {
+      beforeEach(() => {
+        setupCtx(() => {
+          ctrl.range.from = moment([2015, 1, 1]);
+          ctrl.range.to = moment([2015, 11, 20]);
+        });
+      });
+
+      it('should format dates as month days', function() {
+        var axis = ctx.plotOptions.xaxis;
+        expect(axis.timeformat).toBe('%m/%d');
+      });
+    });
+  });
+
+  describe('when graph is histogram, and enable stack', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = true;
+        ctrl.hiddenSeries = {};
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+      expect(ctx.plotData[1].data[0][0]).toBe(100);
+      expect(ctx.plotData[1].data[0][1]).toBe(2);
+    });
+  });
+
+  describe('when graph is histogram, and some series are hidden', () => {
+    beforeEach(() => {
+      setupCtx(() => {
+        ctrl.panel.xaxis.mode = 'histogram';
+        ctrl.panel.stack = false;
+        ctrl.hiddenSeries = { series2: true };
+        ctx.data[0] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series1',
+        });
+        ctx.data[1] = new TimeSeries({
+          datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
+          alias: 'series2',
+        });
+      });
+    });
+
+    it('should calculate correct histogram', function() {
+      expect(ctx.plotData[0].data[0][0]).toBe(100);
+      expect(ctx.plotData[0].data[0][1]).toBe(2);
+    });
+  });
+});

+ 0 - 454
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -1,454 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-import angular from 'angular';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-import { Emitter } from 'app/core/core';
-
-describe('grafanaGraph', function() {
-  beforeEach(angularMocks.module('grafana.core'));
-
-  function graphScenario(desc, func, elementWidth = 500) {
-    describe(desc, () => {
-      var ctx: any = {};
-
-      ctx.setup = setupFunc => {
-        beforeEach(
-          angularMocks.module($provide => {
-            $provide.value('timeSrv', new helpers.TimeSrvStub());
-          })
-        );
-
-        beforeEach(
-          angularMocks.inject(($rootScope, $compile) => {
-            var ctrl: any = {
-              height: 200,
-              panel: {
-                events: new Emitter(),
-                legend: {},
-                grid: {},
-                yaxes: [
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                  {
-                    min: null,
-                    max: null,
-                    format: 'short',
-                    logBase: 1,
-                  },
-                ],
-                thresholds: [],
-                xaxis: {},
-                seriesOverrides: [],
-                tooltip: {
-                  shared: true,
-                },
-              },
-              renderingCompleted: sinon.spy(),
-              hiddenSeries: {},
-              dashboard: {
-                getTimezone: sinon.stub().returns('browser'),
-              },
-              range: {
-                from: moment([2015, 1, 1, 10]),
-                to: moment([2015, 1, 1, 22]),
-              },
-            };
-
-            var scope = $rootScope.$new();
-            scope.ctrl = ctrl;
-            scope.ctrl.events = ctrl.panel.events;
-
-            $rootScope.onAppEvent = sinon.spy();
-
-            ctx.data = [];
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[1, 1], [2, 2]],
-                alias: 'series1',
-              })
-            );
-            ctx.data.push(
-              new TimeSeries({
-                datapoints: [[10, 1], [20, 2]],
-                alias: 'series2',
-              })
-            );
-
-            setupFunc(ctrl, ctx.data);
-
-            var element = angular.element("<div style='width:" + elementWidth + "px' grafana-graph><div>");
-            $compile(element)(scope);
-            scope.$digest();
-
-            $.plot = ctx.plotSpy = sinon.spy();
-            ctrl.events.emit('render', ctx.data);
-            ctrl.events.emit('render-legend');
-            ctrl.events.emit('legend-rendering-complete');
-            ctx.plotData = ctx.plotSpy.getCall(0).args[1];
-            ctx.plotOptions = ctx.plotSpy.getCall(0).args[2];
-          })
-        );
-      };
-
-      func(ctx);
-    });
-  }
-
-  graphScenario('simple lines options', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      ctrl.panel.linewidth = 3;
-      ctrl.panel.steppedLine = true;
-    });
-
-    it('should configure plot with correct options', () => {
-      expect(ctx.plotOptions.series.lines.show).to.be(true);
-      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-      expect(ctx.plotOptions.series.lines.lineWidth).to.be(3);
-      expect(ctx.plotOptions.series.lines.steps).to.be(true);
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = undefined;
-      ctrl.panel.stack = false;
-    });
-
-    it('should not modify order of time series', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. min ascending order', ctx => {
-    ctx.setup((ctrl, data) => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = false;
-      ctrl.panel.stack = true;
-    });
-
-    it('lowest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. stacking disabled', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'min';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = false;
-    });
-
-    it('highest value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].alias).to.be('series2');
-    });
-  });
-
-  graphScenario('sorting stacked series as legend. current descending order', ctx => {
-    ctx.setup(ctrl => {
-      ctrl.panel.legend.sort = 'current';
-      ctrl.panel.legend.sortDesc = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('highest last value should be first', () => {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when logBase is log 10', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-      ctrl.panel.yaxes[1].logBase = 10;
-      ctrl.panel.yaxes[1].min = '0.05';
-      ctrl.panel.yaxes[1].max = '1500';
-      data[1] = new TimeSeries({
-        datapoints: [[2000, 1], [0.002, 2], [0, 3], [-1, 4]],
-        alias: 'seriesFixedscale',
-      });
-      data[1].yaxis = 2;
-    });
-
-    it('should apply axis transform, autoscaling (if necessary) and ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.within(7, 8);
-      expect(axisAutoscale.ticks[0]).to.within(0.00099999999, 0.00100000001);
-      if (axisAutoscale.ticks.length === 7) {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.within(999.9999, 1000.0001);
-      } else {
-        expect(axisAutoscale.ticks[axisAutoscale.ticks.length - 1]).to.be(10000);
-      }
-
-      var axisFixedscale = ctx.plotOptions.yaxes[1];
-      expect(axisFixedscale.min).to.be(0.05);
-      expect(axisFixedscale.max).to.be(1500);
-      expect(axisFixedscale.ticks.length).to.be(5);
-      expect(axisFixedscale.ticks[0]).to.be(0.1);
-      expect(axisFixedscale.ticks[4]).to.be(1000);
-    });
-  });
-
-  graphScenario('when logBase is log 10 and data points contain only zeroes', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      data[0] = new TimeSeries({
-        datapoints: [[0, 1], [0, 2], [0, 3], [0, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should not set min and max and should create some fake ticks', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(undefined);
-      expect(axisAutoscale.max).to.be(undefined);
-      expect(axisAutoscale.ticks.length).to.be(2);
-      expect(axisAutoscale.ticks[0]).to.be(1);
-      expect(axisAutoscale.ticks[1]).to.be(2);
-    });
-  });
-
-  // y-min set 0 is a special case for log scale,
-  // this approximates it by setting min to 0.1
-  graphScenario('when logBase is log 10 and y-min is set to 0 and auto min is > 0.1', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.yaxes[0].logBase = 10;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should set min to 0.1 and add a tick for 0.1', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.transform(100)).to.be(2);
-      expect(axisAutoscale.inverseTransform(-3)).to.within(0.00099999999, 0.00100000001);
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.max).to.be(10000);
-      expect(axisAutoscale.ticks.length).to.be(6);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[5]).to.be(10000);
-    });
-  });
-
-  graphScenario('when logBase is log 2 and y-min is set to 0 and num of ticks exceeds max', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      const heightForApprox5Ticks = 125;
-      ctrl.height = heightForApprox5Ticks;
-      ctrl.panel.yaxes[0].logBase = 2;
-      ctrl.panel.yaxes[0].min = '0';
-      data[0] = new TimeSeries({
-        datapoints: [[2000, 1], [4, 2], [500, 3], [3000, 4], [10000, 5], [100000, 6]],
-        alias: 'seriesAutoscale',
-      });
-      data[0].yaxis = 1;
-    });
-
-    it('should regenerate ticks so that if fits on the y-axis', function() {
-      var axisAutoscale = ctx.plotOptions.yaxes[0];
-      expect(axisAutoscale.min).to.be(0.1);
-      expect(axisAutoscale.ticks.length).to.be(8);
-      expect(axisAutoscale.ticks[0]).to.be(0.1);
-      expect(axisAutoscale.ticks[7]).to.be(262144);
-      expect(axisAutoscale.max).to.be(262144);
-    });
-
-    it('should set axis max to be max tick value', function() {
-      expect(ctx.plotOptions.yaxes[0].max).to.be(262144);
-    });
-  });
-
-  graphScenario('dashed lines options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.lines = true;
-      ctrl.panel.linewidth = 2;
-      ctrl.panel.dashes = true;
-    });
-
-    it('should configure dashed plot with correct options', function() {
-      expect(ctx.plotOptions.series.lines.show).to.be(true);
-      expect(ctx.plotOptions.series.dashes.lineWidth).to.be(2);
-      expect(ctx.plotOptions.series.dashes.show).to.be(true);
-    });
-  });
-
-  graphScenario('should use timeStep for barWidth', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.bars = true;
-      data[0] = new TimeSeries({
-        datapoints: [[1, 10], [2, 20]],
-        alias: 'series1',
-      });
-    });
-
-    it('should set barWidth', function() {
-      expect(ctx.plotOptions.series.bars.barWidth).to.be(1 / 1.5);
-    });
-  });
-
-  graphScenario('series option overrides, fill & points', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.lines = true;
-      ctrl.panel.fill = 5;
-      data[0].zindex = 10;
-      data[1].alias = 'test';
-      data[1].lines = { fill: 0.001 };
-      data[1].points = { show: true };
-    });
-
-    it('should match second series and fill zero, and enable points', function() {
-      expect(ctx.plotOptions.series.lines.fill).to.be(0.5);
-      expect(ctx.plotData[1].lines.fill).to.be(0.001);
-      expect(ctx.plotData[1].points.show).to.be(true);
-    });
-  });
-
-  graphScenario('should order series order according to zindex', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      data[1].zindex = 1;
-      data[0].zindex = 10;
-    });
-
-    it('should move zindex 2 last', function() {
-      expect(ctx.plotData[0].alias).to.be('series2');
-      expect(ctx.plotData[1].alias).to.be('series1');
-    });
-  });
-
-  graphScenario('when series is hidden', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.hiddenSeries = { series2: true };
-    });
-
-    it('should remove datapoints and disable stack', function() {
-      expect(ctx.plotData[0].alias).to.be('series1');
-      expect(ctx.plotData[1].data.length).to.be(0);
-      expect(ctx.plotData[1].stack).to.be(false);
-    });
-  });
-
-  graphScenario('when stack and percent', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.percentage = true;
-      ctrl.panel.stack = true;
-    });
-
-    it('should show percentage', function() {
-      var axis = ctx.plotOptions.yaxes[0];
-      expect(axis.tickFormatter(100, axis)).to.be('100%');
-    });
-  });
-
-  graphScenario(
-    'when panel too narrow to show x-axis dates in same granularity as wide panels',
-    function(ctx) {
-      describe('and the range is less than 24 hours', function() {
-        ctx.setup(function(ctrl) {
-          ctrl.range.from = moment([2015, 1, 1, 10]);
-          ctrl.range.to = moment([2015, 1, 1, 22]);
-        });
-
-        it('should format dates as hours minutes', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%H:%M');
-        });
-      });
-
-      describe('and the range is less than one year', function() {
-        ctx.setup(function(scope) {
-          scope.range.from = moment([2015, 1, 1]);
-          scope.range.to = moment([2015, 11, 20]);
-        });
-
-        it('should format dates as month days', function() {
-          var axis = ctx.plotOptions.xaxis;
-          expect(axis.timeformat).to.be('%m/%d');
-        });
-      });
-    },
-    10
-  );
-
-  graphScenario('when graph is histogram, and enable stack', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = true;
-      ctrl.hiddenSeries = {};
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-      expect(ctx.plotData[1].data[0][0]).to.be(100);
-      expect(ctx.plotData[1].data[0][1]).to.be(2);
-    });
-  });
-
-  graphScenario('when graph is histogram, and some series are hidden', function(ctx) {
-    ctx.setup(function(ctrl, data) {
-      ctrl.panel.xaxis.mode = 'histogram';
-      ctrl.panel.stack = false;
-      ctrl.hiddenSeries = { series2: true };
-      data[0] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series1',
-      });
-      data[1] = new TimeSeries({
-        datapoints: [[100, 1], [100, 2], [200, 3], [300, 4]],
-        alias: 'series2',
-      });
-    });
-
-    it('should calculate correct histogram', function() {
-      expect(ctx.plotData[0].data[0][0]).to.be(100);
-      expect(ctx.plotData[0].data[0][1]).to.be(2);
-    });
-  });
-});

+ 361 - 350
public/app/plugins/panel/heatmap/rendering.ts

@@ -19,56 +19,94 @@ let MIN_CARD_SIZE = 1,
   Y_AXIS_TICK_PADDING = 5,
   MIN_SELECTION_WIDTH = 2;
 
-export default function link(scope, elem, attrs, ctrl) {
-  let data, timeRange, panel, heatmap;
-
-  // $heatmap is JQuery object, but heatmap is D3
-  let $heatmap = elem.find('.heatmap-panel');
-  let tooltip = new HeatmapTooltip($heatmap, scope);
-
-  let width,
-    height,
-    yScale,
-    xScale,
-    chartWidth,
-    chartHeight,
-    chartTop,
-    chartBottom,
-    yAxisWidth,
-    xAxisHeight,
-    cardPadding,
-    cardRound,
-    cardWidth,
-    cardHeight,
-    colorScale,
-    opacityScale,
-    mouseUpHandler;
-
-  let selection = {
-    active: false,
-    x1: -1,
-    x2: -1,
-  };
-
-  let padding = { left: 0, right: 0, top: 0, bottom: 0 },
-    margin = { left: 25, right: 15, top: 10, bottom: 20 },
-    dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
-
-  ctrl.events.on('render', () => {
-    render();
-    ctrl.renderingCompleted();
-  });
-
-  function setElementHeight() {
+export default function rendering(scope, elem, attrs, ctrl) {
+  return new HeatmapRenderer(scope, elem, attrs, ctrl);
+}
+export class HeatmapRenderer {
+  width: number;
+  height: number;
+  yScale: any;
+  xScale: any;
+  chartWidth: number;
+  chartHeight: number;
+  chartTop: number;
+  chartBottom: number;
+  yAxisWidth: number;
+  xAxisHeight: number;
+  cardPadding: number;
+  cardRound: number;
+  cardWidth: number;
+  cardHeight: number;
+  colorScale: any;
+  opacityScale: any;
+  mouseUpHandler: any;
+  data: any;
+  panel: any;
+  $heatmap: any;
+  tooltip: HeatmapTooltip;
+  heatmap: any;
+  timeRange: any;
+
+  selection: any;
+  padding: any;
+  margin: any;
+  dataRangeWidingFactor: number;
+  constructor(private scope, private elem, attrs, private ctrl) {
+    // $heatmap is JQuery object, but heatmap is D3
+    this.$heatmap = this.elem.find('.heatmap-panel');
+    this.tooltip = new HeatmapTooltip(this.$heatmap, this.scope);
+
+    this.selection = {
+      active: false,
+      x1: -1,
+      x2: -1,
+    };
+
+    this.padding = { left: 0, right: 0, top: 0, bottom: 0 };
+    this.margin = { left: 25, right: 15, top: 10, bottom: 20 };
+    this.dataRangeWidingFactor = DATA_RANGE_WIDING_FACTOR;
+
+    this.ctrl.events.on('render', this.onRender.bind(this));
+
+    this.ctrl.tickValueFormatter = this.tickValueFormatter.bind(this);
+    /////////////////////////////
+    // Selection and crosshair //
+    /////////////////////////////
+
+    // Shared crosshair and tooltip
+    appEvents.on('graph-hover', this.onGraphHover.bind(this), this.scope);
+
+    appEvents.on('graph-hover-clear', this.onGraphHoverClear.bind(this), this.scope);
+
+    // Register selection listeners
+    this.$heatmap.on('mousedown', this.onMouseDown.bind(this));
+    this.$heatmap.on('mousemove', this.onMouseMove.bind(this));
+    this.$heatmap.on('mouseleave', this.onMouseLeave.bind(this));
+  }
+
+  onGraphHoverClear() {
+    this.clearCrosshair();
+  }
+
+  onGraphHover(event) {
+    this.drawSharedCrosshair(event.pos);
+  }
+
+  onRender() {
+    this.render();
+    this.ctrl.renderingCompleted();
+  }
+
+  setElementHeight() {
     try {
-      var height = ctrl.height || panel.height || ctrl.row.height;
+      var height = this.ctrl.height || this.panel.height || this.ctrl.row.height;
       if (_.isString(height)) {
         height = parseInt(height.replace('px', ''), 10);
       }
 
-      height -= panel.legend.show ? 28 : 11; // bottom padding and space for legend
+      height -= this.panel.legend.show ? 28 : 11; // bottom padding and space for legend
 
-      $heatmap.css('height', height + 'px');
+      this.$heatmap.css('height', height + 'px');
 
       return true;
     } catch (e) {
@@ -77,7 +115,7 @@ export default function link(scope, elem, attrs, ctrl) {
     }
   }
 
-  function getYAxisWidth(elem) {
+  getYAxisWidth(elem) {
     let axis_text = elem.selectAll('.axis-y text').nodes();
     let max_text_width = _.max(
       _.map(axis_text, text => {
@@ -89,7 +127,7 @@ export default function link(scope, elem, attrs, ctrl) {
     return max_text_width;
   }
 
-  function getXAxisHeight(elem) {
+  getXAxisHeight(elem) {
     let axis_line = elem.select('.axis-x line');
     if (!axis_line.empty()) {
       let axis_line_position = parseFloat(elem.select('.axis-x line').attr('y2'));
@@ -101,16 +139,16 @@ export default function link(scope, elem, attrs, ctrl) {
     }
   }
 
-  function addXAxis() {
-    scope.xScale = xScale = d3
+  addXAxis() {
+    this.scope.xScale = this.xScale = d3
       .scaleTime()
-      .domain([timeRange.from, timeRange.to])
-      .range([0, chartWidth]);
+      .domain([this.timeRange.from, this.timeRange.to])
+      .range([0, this.chartWidth]);
 
-    let ticks = chartWidth / DEFAULT_X_TICK_SIZE_PX;
-    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, timeRange.from, timeRange.to);
+    let ticks = this.chartWidth / DEFAULT_X_TICK_SIZE_PX;
+    let grafanaTimeFormatter = ticksUtils.grafanaTimeFormat(ticks, this.timeRange.from, this.timeRange.to);
     let timeFormat;
-    let dashboardTimeZone = ctrl.dashboard.getTimezone();
+    let dashboardTimeZone = this.ctrl.dashboard.getTimezone();
     if (dashboardTimeZone === 'utc') {
       timeFormat = d3.utcFormat(grafanaTimeFormatter);
     } else {
@@ -118,100 +156,100 @@ export default function link(scope, elem, attrs, ctrl) {
     }
 
     let xAxis = d3
-      .axisBottom(xScale)
+      .axisBottom(this.xScale)
       .ticks(ticks)
       .tickFormat(timeFormat)
       .tickPadding(X_AXIS_TICK_PADDING)
-      .tickSize(chartHeight);
+      .tickSize(this.chartHeight);
 
-    let posY = margin.top;
-    let posX = yAxisWidth;
-    heatmap
+    let posY = this.margin.top;
+    let posX = this.yAxisWidth;
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-x')
       .attr('transform', 'translate(' + posX + ',' + posY + ')')
       .call(xAxis);
 
     // Remove horizontal line in the top of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-x')
       .select('.domain')
       .remove();
   }
 
-  function addYAxis() {
-    let ticks = Math.ceil(chartHeight / DEFAULT_Y_TICK_SIZE_PX);
-    let tick_interval = ticksUtils.tickStep(data.heatmapStats.min, data.heatmapStats.max, ticks);
-    let { y_min, y_max } = wideYAxisRange(data.heatmapStats.min, data.heatmapStats.max, tick_interval);
+  addYAxis() {
+    let ticks = Math.ceil(this.chartHeight / DEFAULT_Y_TICK_SIZE_PX);
+    let tick_interval = ticksUtils.tickStep(this.data.heatmapStats.min, this.data.heatmapStats.max, ticks);
+    let { y_min, y_max } = this.wideYAxisRange(this.data.heatmapStats.min, this.data.heatmapStats.max, tick_interval);
 
     // Rewrite min and max if it have been set explicitly
-    y_min = panel.yAxis.min !== null ? panel.yAxis.min : y_min;
-    y_max = panel.yAxis.max !== null ? panel.yAxis.max : y_max;
+    y_min = this.panel.yAxis.min !== null ? this.panel.yAxis.min : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.panel.yAxis.max : y_max;
 
     // Adjust ticks after Y range widening
     tick_interval = ticksUtils.tickStep(y_min, y_max, ticks);
     ticks = Math.ceil((y_max - y_min) / tick_interval);
 
     let decimalsAuto = ticksUtils.getPrecision(tick_interval);
-    let decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
+    let decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, ticks, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = 1;
       y_min = -1;
       ticks = 3;
       decimals = 1;
     }
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       max: y_max,
       ticks: ticks,
     };
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .ticks(ticks)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
   // Wide Y values range and anjust to bucket size
-  function wideYAxisRange(min, max, tickInterval) {
-    let y_widing = (max * (dataRangeWidingFactor - 1) - min * (dataRangeWidingFactor - 1)) / 2;
+  wideYAxisRange(min, max, tickInterval) {
+    let y_widing = (max * (this.dataRangeWidingFactor - 1) - min * (this.dataRangeWidingFactor - 1)) / 2;
     let y_min, y_max;
 
     if (tickInterval === 0) {
-      y_max = max * dataRangeWidingFactor;
-      y_min = min - min * (dataRangeWidingFactor - 1);
+      y_max = max * this.dataRangeWidingFactor;
+      y_min = min - min * (this.dataRangeWidingFactor - 1);
       tickInterval = (y_max - y_min) / 2;
     } else {
       y_max = Math.ceil((max + y_widing) / tickInterval) * tickInterval;
@@ -226,89 +264,91 @@ export default function link(scope, elem, attrs, ctrl) {
     return { y_min, y_max };
   }
 
-  function addLogYAxis() {
-    let log_base = panel.yAxis.logBase;
-    let { y_min, y_max } = adjustLogRange(data.heatmapStats.minLog, data.heatmapStats.max, log_base);
+  addLogYAxis() {
+    let log_base = this.panel.yAxis.logBase;
+    let { y_min, y_max } = this.adjustLogRange(this.data.heatmapStats.minLog, this.data.heatmapStats.max, log_base);
 
-    y_min = panel.yAxis.min && panel.yAxis.min !== '0' ? adjustLogMin(panel.yAxis.min, log_base) : y_min;
-    y_max = panel.yAxis.max !== null ? adjustLogMax(panel.yAxis.max, log_base) : y_max;
+    y_min =
+      this.panel.yAxis.min && this.panel.yAxis.min !== '0' ? this.adjustLogMin(this.panel.yAxis.min, log_base) : y_min;
+    y_max = this.panel.yAxis.max !== null ? this.adjustLogMax(this.panel.yAxis.max, log_base) : y_max;
 
     // Set default Y min and max if no data
-    if (_.isEmpty(data.buckets)) {
+    if (_.isEmpty(this.data.buckets)) {
       y_max = Math.pow(log_base, 2);
       y_min = 1;
     }
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLog()
-      .base(panel.yAxis.logBase)
+      .base(this.panel.yAxis.logBase)
       .domain([y_min, y_max])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
-    let domain = yScale.domain();
-    let tick_values = logScaleTickValues(domain, log_base);
+    let domain = this.yScale.domain();
+    let tick_values = this.logScaleTickValues(domain, log_base);
 
     let decimalsAuto = ticksUtils.getPrecision(y_min);
-    let decimals = panel.yAxis.decimals || decimalsAuto;
+    let decimals = this.panel.yAxis.decimals || decimalsAuto;
 
     // Calculate scaledDecimals for log scales using tick size (as in jquery.flot.js)
     let flot_tick_size = ticksUtils.getFlotTickSize(y_min, y_max, tick_values.length, decimalsAuto);
     let scaledDecimals = ticksUtils.getScaledDecimals(decimals, flot_tick_size);
-    ctrl.decimals = decimals;
-    ctrl.scaledDecimals = scaledDecimals;
+    this.ctrl.decimals = decimals;
+    this.ctrl.scaledDecimals = scaledDecimals;
 
-    data.yAxis = {
+    this.data.yAxis = {
       min: y_min,
       max: y_max,
       ticks: tick_values.length,
     };
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
-      .tickFormat(tickValueFormatter(decimals, scaledDecimals))
-      .tickSizeInner(0 - width)
+      .tickFormat(this.tickValueFormatter(decimals, scaledDecimals))
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    let posY = margin.top;
-    let posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    let posY = this.margin.top;
+    let posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Set first tick as pseudo 0
     if (y_min < 1) {
-      heatmap
+      this.heatmap
         .select('.axis-y')
         .select('.tick text')
         .text('0');
     }
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
-  function addYAxisFromBuckets() {
-    const tsBuckets = data.tsBuckets;
+  addYAxisFromBuckets() {
+    const tsBuckets = this.data.tsBuckets;
 
-    scope.yScale = yScale = d3
+    this.scope.yScale = this.yScale = d3
       .scaleLinear()
       .domain([0, tsBuckets.length - 1])
-      .range([chartHeight, 0]);
+      .range([this.chartHeight, 0]);
 
     const tick_values = _.map(tsBuckets, (b, i) => i);
     const decimalsAuto = _.max(_.map(tsBuckets, ticksUtils.getStringPrecision));
-    const decimals = panel.yAxis.decimals === null ? decimalsAuto : panel.yAxis.decimals;
-    ctrl.decimals = decimals;
+    const decimals = this.panel.yAxis.decimals === null ? decimalsAuto : this.panel.yAxis.decimals;
+    this.ctrl.decimals = decimals;
 
+    let tickValueFormatter = this.tickValueFormatter.bind(this);
     function tickFormatter(valIndex) {
       let valueFormatted = tsBuckets[valIndex];
       if (!_.isNaN(_.toNumber(valueFormatted)) && valueFormatted !== '') {
@@ -319,59 +359,59 @@ export default function link(scope, elem, attrs, ctrl) {
     }
 
     const tsBucketsFormatted = _.map(tsBuckets, (v, i) => tickFormatter(i));
-    data.tsBucketsFormatted = tsBucketsFormatted;
+    this.data.tsBucketsFormatted = tsBucketsFormatted;
 
     let yAxis = d3
-      .axisLeft(yScale)
+      .axisLeft(this.yScale)
       .tickValues(tick_values)
       .tickFormat(tickFormatter)
-      .tickSizeInner(0 - width)
+      .tickSizeInner(0 - this.width)
       .tickSizeOuter(0)
       .tickPadding(Y_AXIS_TICK_PADDING);
 
-    heatmap
+    this.heatmap
       .append('g')
       .attr('class', 'axis axis-y')
       .call(yAxis);
 
     // Calculate Y axis width first, then move axis into visible area
-    const posY = margin.top;
-    const posX = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
+    const posY = this.margin.top;
+    const posX = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.heatmap.select('.axis-y').attr('transform', 'translate(' + posX + ',' + posY + ')');
 
     // Remove vertical line in the right of axis labels (called domain in d3)
-    heatmap
+    this.heatmap
       .select('.axis-y')
       .select('.domain')
       .remove();
   }
 
   // Adjust data range to log base
-  function adjustLogRange(min, max, logBase) {
+  adjustLogRange(min, max, logBase) {
     let y_min, y_max;
 
-    y_min = data.heatmapStats.minLog;
-    if (data.heatmapStats.minLog > 1 || !data.heatmapStats.minLog) {
+    y_min = this.data.heatmapStats.minLog;
+    if (this.data.heatmapStats.minLog > 1 || !this.data.heatmapStats.minLog) {
       y_min = 1;
     } else {
-      y_min = adjustLogMin(data.heatmapStats.minLog, logBase);
+      y_min = this.adjustLogMin(this.data.heatmapStats.minLog, logBase);
     }
 
     // Adjust max Y value to log base
-    y_max = adjustLogMax(data.heatmapStats.max, logBase);
+    y_max = this.adjustLogMax(this.data.heatmapStats.max, logBase);
 
     return { y_min, y_max };
   }
 
-  function adjustLogMax(max, base) {
+  adjustLogMax(max, base) {
     return Math.pow(base, Math.ceil(ticksUtils.logp(max, base)));
   }
 
-  function adjustLogMin(min, base) {
+  adjustLogMin(min, base) {
     return Math.pow(base, Math.floor(ticksUtils.logp(min, base)));
   }
 
-  function logScaleTickValues(domain, base) {
+  logScaleTickValues(domain, base) {
     let domainMin = domain[0];
     let domainMax = domain[1];
     let tickValues = [];
@@ -393,8 +433,8 @@ export default function link(scope, elem, attrs, ctrl) {
     return tickValues;
   }
 
-  function tickValueFormatter(decimals, scaledDecimals = null) {
-    let format = panel.yAxis.format;
+  tickValueFormatter(decimals, scaledDecimals = null) {
+    let format = this.panel.yAxis.format;
     return function(value) {
       try {
         return format !== 'none' ? kbn.valueFormats[format](value, decimals, scaledDecimals) : value;
@@ -405,181 +445,178 @@ export default function link(scope, elem, attrs, ctrl) {
     };
   }
 
-  ctrl.tickValueFormatter = tickValueFormatter;
-
-  function fixYAxisTickSize() {
-    heatmap
+  fixYAxisTickSize() {
+    this.heatmap
       .select('.axis-y')
       .selectAll('.tick line')
-      .attr('x2', chartWidth);
+      .attr('x2', this.chartWidth);
   }
 
-  function addAxes() {
-    chartHeight = height - margin.top - margin.bottom;
-    chartTop = margin.top;
-    chartBottom = chartTop + chartHeight;
-
-    if (panel.dataFormat === 'tsbuckets') {
-      addYAxisFromBuckets();
+  addAxes() {
+    this.chartHeight = this.height - this.margin.top - this.margin.bottom;
+    this.chartTop = this.margin.top;
+    this.chartBottom = this.chartTop + this.chartHeight;
+    if (this.panel.dataFormat === 'tsbuckets') {
+      this.addYAxisFromBuckets();
     } else {
-      if (panel.yAxis.logBase === 1) {
-        addYAxis();
+      if (this.panel.yAxis.logBase === 1) {
+        this.addYAxis();
       } else {
-        addLogYAxis();
+        this.addLogYAxis();
       }
     }
 
-    yAxisWidth = getYAxisWidth(heatmap) + Y_AXIS_TICK_PADDING;
-    chartWidth = width - yAxisWidth - margin.right;
-    fixYAxisTickSize();
+    this.yAxisWidth = this.getYAxisWidth(this.heatmap) + Y_AXIS_TICK_PADDING;
+    this.chartWidth = this.width - this.yAxisWidth - this.margin.right;
+    this.fixYAxisTickSize();
 
-    addXAxis();
-    xAxisHeight = getXAxisHeight(heatmap);
+    this.addXAxis();
+    this.xAxisHeight = this.getXAxisHeight(this.heatmap);
 
-    if (!panel.yAxis.show) {
-      heatmap
+    if (!this.panel.yAxis.show) {
+      this.heatmap
         .select('.axis-y')
         .selectAll('line')
         .style('opacity', 0);
     }
 
-    if (!panel.xAxis.show) {
-      heatmap
+    if (!this.panel.xAxis.show) {
+      this.heatmap
         .select('.axis-x')
         .selectAll('line')
         .style('opacity', 0);
     }
   }
 
-  function addHeatmapCanvas() {
-    let heatmap_elem = $heatmap[0];
+  addHeatmapCanvas() {
+    let heatmap_elem = this.$heatmap[0];
 
-    width = Math.floor($heatmap.width()) - padding.right;
-    height = Math.floor($heatmap.height()) - padding.bottom;
+    this.width = Math.floor(this.$heatmap.width()) - this.padding.right;
+    this.height = Math.floor(this.$heatmap.height()) - this.padding.bottom;
 
-    cardPadding = panel.cards.cardPadding !== null ? panel.cards.cardPadding : CARD_PADDING;
-    cardRound = panel.cards.cardRound !== null ? panel.cards.cardRound : CARD_ROUND;
+    this.cardPadding = this.panel.cards.cardPadding !== null ? this.panel.cards.cardPadding : CARD_PADDING;
+    this.cardRound = this.panel.cards.cardRound !== null ? this.panel.cards.cardRound : CARD_ROUND;
 
-    if (heatmap) {
-      heatmap.remove();
+    if (this.heatmap) {
+      this.heatmap.remove();
     }
 
-    heatmap = d3
+    this.heatmap = d3
       .select(heatmap_elem)
       .append('svg')
-      .attr('width', width)
-      .attr('height', height);
+      .attr('width', this.width)
+      .attr('height', this.height);
   }
 
-  function addHeatmap() {
-    addHeatmapCanvas();
-    addAxes();
+  addHeatmap() {
+    this.addHeatmapCanvas();
+    this.addAxes();
 
-    if (panel.yAxis.logBase !== 1 && panel.dataFormat !== 'tsbuckets') {
-      let log_base = panel.yAxis.logBase;
-      let domain = yScale.domain();
-      let tick_values = logScaleTickValues(domain, log_base);
-      data.buckets = mergeZeroBuckets(data.buckets, _.min(tick_values));
+    if (this.panel.yAxis.logBase !== 1 && this.panel.dataFormat !== 'tsbuckets') {
+      let log_base = this.panel.yAxis.logBase;
+      let domain = this.yScale.domain();
+      let tick_values = this.logScaleTickValues(domain, log_base);
+      this.data.buckets = mergeZeroBuckets(this.data.buckets, _.min(tick_values));
     }
 
-    let cardsData = data.cards;
-    let maxValueAuto = data.cardStats.max;
-    let maxValue = panel.color.max || maxValueAuto;
-    let minValue = panel.color.min || 0;
+    let cardsData = this.data.cards;
+    let maxValueAuto = this.data.cardStats.max;
+    let maxValue = this.panel.color.max || maxValueAuto;
+    let minValue = this.panel.color.min || 0;
 
-    let colorScheme = _.find(ctrl.colorSchemes, {
-      value: panel.color.colorScheme,
+    let colorScheme = _.find(this.ctrl.colorSchemes, {
+      value: this.panel.color.colorScheme,
     });
-    colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
-    opacityScale = getOpacityScale(panel.color, maxValue);
-    setCardSize();
+    this.colorScale = getColorScale(colorScheme, contextSrv.user.lightTheme, maxValue, minValue);
+    this.opacityScale = getOpacityScale(this.panel.color, maxValue);
+    this.setCardSize();
 
-    let cards = heatmap.selectAll('.heatmap-card').data(cardsData);
+    let cards = this.heatmap.selectAll('.heatmap-card').data(cardsData);
     cards.append('title');
     cards = cards
       .enter()
       .append('rect')
-      .attr('x', getCardX)
-      .attr('width', getCardWidth)
-      .attr('y', getCardY)
-      .attr('height', getCardHeight)
-      .attr('rx', cardRound)
-      .attr('ry', cardRound)
+      .attr('x', this.getCardX.bind(this))
+      .attr('width', this.getCardWidth.bind(this))
+      .attr('y', this.getCardY.bind(this))
+      .attr('height', this.getCardHeight.bind(this))
+      .attr('rx', this.cardRound)
+      .attr('ry', this.cardRound)
       .attr('class', 'bordered heatmap-card')
-      .style('fill', getCardColor)
-      .style('stroke', getCardColor)
+      .style('fill', this.getCardColor.bind(this))
+      .style('stroke', this.getCardColor.bind(this))
       .style('stroke-width', 0)
-      .style('opacity', getCardOpacity);
+      .style('opacity', this.getCardOpacity.bind(this));
 
-    let $cards = $heatmap.find('.heatmap-card');
+    let $cards = this.$heatmap.find('.heatmap-card');
     $cards
       .on('mouseenter', event => {
-        tooltip.mouseOverBucket = true;
-        highlightCard(event);
+        this.tooltip.mouseOverBucket = true;
+        this.highlightCard(event);
       })
       .on('mouseleave', event => {
-        tooltip.mouseOverBucket = false;
-        resetCardHighLight(event);
+        this.tooltip.mouseOverBucket = false;
+        this.resetCardHighLight(event);
       });
   }
 
-  function highlightCard(event) {
+  highlightCard(event) {
     let color = d3.select(event.target).style('fill');
     let highlightColor = d3.color(color).darker(2);
     let strokeColor = d3.color(color).brighter(4);
     let current_card = d3.select(event.target);
-    tooltip.originalFillColor = color;
+    this.tooltip.originalFillColor = color;
     current_card
       .style('fill', highlightColor.toString())
       .style('stroke', strokeColor.toString())
       .style('stroke-width', 1);
   }
 
-  function resetCardHighLight(event) {
+  resetCardHighLight(event) {
     d3
       .select(event.target)
-      .style('fill', tooltip.originalFillColor)
-      .style('stroke', tooltip.originalFillColor)
+      .style('fill', this.tooltip.originalFillColor)
+      .style('stroke', this.tooltip.originalFillColor)
       .style('stroke-width', 0);
   }
 
-  function setCardSize() {
-    let xGridSize = Math.floor(xScale(data.xBucketSize) - xScale(0));
-    let yGridSize = Math.floor(yScale(yScale.invert(0) - data.yBucketSize));
+  setCardSize() {
+    let xGridSize = Math.floor(this.xScale(this.data.xBucketSize) - this.xScale(0));
+    let yGridSize = Math.floor(this.yScale(this.yScale.invert(0) - this.data.yBucketSize));
 
-    if (panel.yAxis.logBase !== 1) {
-      let base = panel.yAxis.logBase;
-      let splitFactor = data.yBucketSize || 1;
-      yGridSize = Math.floor((yScale(1) - yScale(base)) / splitFactor);
+    if (this.panel.yAxis.logBase !== 1) {
+      let base = this.panel.yAxis.logBase;
+      let splitFactor = this.data.yBucketSize || 1;
+      yGridSize = Math.floor((this.yScale(1) - this.yScale(base)) / splitFactor);
     }
 
-    cardWidth = xGridSize - cardPadding * 2;
-    cardHeight = yGridSize ? yGridSize - cardPadding * 2 : 0;
+    this.cardWidth = xGridSize - this.cardPadding * 2;
+    this.cardHeight = yGridSize ? yGridSize - this.cardPadding * 2 : 0;
   }
 
-  function getCardX(d) {
+  getCardX(d) {
     let x;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
-      x = yAxisWidth + cardPadding;
+      x = this.yAxisWidth + this.cardPadding;
     } else {
-      x = xScale(d.x) + yAxisWidth + cardPadding;
+      x = this.xScale(d.x) + this.yAxisWidth + this.cardPadding;
     }
 
     return x;
   }
 
-  function getCardWidth(d) {
+  getCardWidth(d) {
     let w;
-    if (xScale(d.x) < 0) {
+    if (this.xScale(d.x) < 0) {
       // Cut card left to prevent overlay
-      let cutted_width = xScale(d.x) + cardWidth;
+      let cutted_width = this.xScale(d.x) + this.cardWidth;
       w = cutted_width > 0 ? cutted_width : 0;
-    } else if (xScale(d.x) + cardWidth > chartWidth) {
+    } else if (this.xScale(d.x) + this.cardWidth > this.chartWidth) {
       // Cut card right to prevent overlay
-      w = chartWidth - xScale(d.x) - cardPadding;
+      w = this.chartWidth - this.xScale(d.x) - this.cardPadding;
     } else {
-      w = cardWidth;
+      w = this.cardWidth;
     }
 
     // Card width should be MIN_CARD_SIZE at least
@@ -587,138 +624,117 @@ export default function link(scope, elem, attrs, ctrl) {
     return w;
   }
 
-  function getCardY(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      y = chartBottom - cardHeight - cardPadding;
+  getCardY(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      y = this.chartBottom - this.cardHeight - this.cardPadding;
     } else {
-      if (y < chartTop) {
-        y = chartTop;
+      if (y < this.chartTop) {
+        y = this.chartTop;
       }
     }
 
     return y;
   }
 
-  function getCardHeight(d) {
-    let y = yScale(d.y) + chartTop - cardHeight - cardPadding;
-    let h = cardHeight;
+  getCardHeight(d) {
+    let y = this.yScale(d.y) + this.chartTop - this.cardHeight - this.cardPadding;
+    let h = this.cardHeight;
 
-    if (panel.yAxis.logBase !== 1 && d.y === 0) {
-      return cardHeight;
+    if (this.panel.yAxis.logBase !== 1 && d.y === 0) {
+      return this.cardHeight;
     }
 
     // Cut card height to prevent overlay
-    if (y < chartTop) {
-      h = yScale(d.y) - cardPadding;
-    } else if (yScale(d.y) > chartBottom) {
-      h = chartBottom - y;
-    } else if (y + cardHeight > chartBottom) {
-      h = chartBottom - y;
+    if (y < this.chartTop) {
+      h = this.yScale(d.y) - this.cardPadding;
+    } else if (this.yScale(d.y) > this.chartBottom) {
+      h = this.chartBottom - y;
+    } else if (y + this.cardHeight > this.chartBottom) {
+      h = this.chartBottom - y;
     }
 
     // Height can't be more than chart height
-    h = Math.min(h, chartHeight);
+    h = Math.min(h, this.chartHeight);
     // Card height should be MIN_CARD_SIZE at least
     h = Math.max(h, MIN_CARD_SIZE);
 
     return h;
   }
 
-  function getCardColor(d) {
-    if (panel.color.mode === 'opacity') {
-      return panel.color.cardColor;
+  getCardColor(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.panel.color.cardColor;
     } else {
-      return colorScale(d.count);
+      return this.colorScale(d.count);
     }
   }
 
-  function getCardOpacity(d) {
-    if (panel.color.mode === 'opacity') {
-      return opacityScale(d.count);
+  getCardOpacity(d) {
+    if (this.panel.color.mode === 'opacity') {
+      return this.opacityScale(d.count);
     } else {
       return 1;
     }
   }
 
-  /////////////////////////////
-  // Selection and crosshair //
-  /////////////////////////////
-
-  // Shared crosshair and tooltip
-  appEvents.on(
-    'graph-hover',
-    event => {
-      drawSharedCrosshair(event.pos);
-    },
-    scope
-  );
-
-  appEvents.on(
-    'graph-hover-clear',
-    () => {
-      clearCrosshair();
-    },
-    scope
-  );
+  onMouseDown(event) {
+    this.selection.active = true;
+    this.selection.x1 = event.offsetX;
 
-  function onMouseDown(event) {
-    selection.active = true;
-    selection.x1 = event.offsetX;
-
-    mouseUpHandler = function() {
-      onMouseUp();
+    this.mouseUpHandler = () => {
+      this.onMouseUp();
     };
 
-    $(document).one('mouseup', mouseUpHandler);
+    $(document).one('mouseup', this.mouseUpHandler.bind(this));
   }
 
-  function onMouseUp() {
-    $(document).unbind('mouseup', mouseUpHandler);
-    mouseUpHandler = null;
-    selection.active = false;
+  onMouseUp() {
+    $(document).unbind('mouseup', this.mouseUpHandler.bind(this));
+    this.mouseUpHandler = null;
+    this.selection.active = false;
 
-    let selectionRange = Math.abs(selection.x2 - selection.x1);
-    if (selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
-      let timeFrom = xScale.invert(Math.min(selection.x1, selection.x2) - yAxisWidth);
-      let timeTo = xScale.invert(Math.max(selection.x1, selection.x2) - yAxisWidth);
+    let selectionRange = Math.abs(this.selection.x2 - this.selection.x1);
+    if (this.selection.x2 >= 0 && selectionRange > MIN_SELECTION_WIDTH) {
+      let timeFrom = this.xScale.invert(Math.min(this.selection.x1, this.selection.x2) - this.yAxisWidth);
+      let timeTo = this.xScale.invert(Math.max(this.selection.x1, this.selection.x2) - this.yAxisWidth);
 
-      ctrl.timeSrv.setTime({
+      this.ctrl.timeSrv.setTime({
         from: moment.utc(timeFrom),
         to: moment.utc(timeTo),
       });
     }
 
-    clearSelection();
+    this.clearSelection();
   }
 
-  function onMouseLeave() {
+  onMouseLeave() {
     appEvents.emit('graph-hover-clear');
-    clearCrosshair();
+    this.clearCrosshair();
   }
 
-  function onMouseMove(event) {
-    if (!heatmap) {
+  onMouseMove(event) {
+    if (!this.heatmap) {
       return;
     }
 
-    if (selection.active) {
+    if (this.selection.active) {
       // Clear crosshair and tooltip
-      clearCrosshair();
-      tooltip.destroy();
+      this.clearCrosshair();
+      this.tooltip.destroy();
 
-      selection.x2 = limitSelection(event.offsetX);
-      drawSelection(selection.x1, selection.x2);
+      this.selection.x2 = this.limitSelection(event.offsetX);
+      this.drawSelection(this.selection.x1, this.selection.x2);
     } else {
-      emitGraphHoverEvent(event);
-      drawCrosshair(event.offsetX);
-      tooltip.show(event, data);
+      this.emitGraphHoverEvent(event);
+      this.drawCrosshair(event.offsetX);
+      this.tooltip.show(event, this.data);
     }
   }
 
-  function emitGraphHoverEvent(event) {
-    let x = xScale.invert(event.offsetX - yAxisWidth).valueOf();
-    let y = yScale.invert(event.offsetY);
+  emitGraphHoverEvent(event) {
+    let x = this.xScale.invert(event.offsetX - this.yAxisWidth).valueOf();
+    let y = this.yScale.invert(event.offsetY);
     let pos = {
       pageX: event.pageX,
       pageY: event.pageY,
@@ -730,105 +746,100 @@ export default function link(scope, elem, attrs, ctrl) {
     };
 
     // Set minimum offset to prevent showing legend from another panel
-    pos.panelRelY = Math.max(event.offsetY / height, 0.001);
+    pos.panelRelY = Math.max(event.offsetY / this.height, 0.001);
 
     // broadcast to other graph panels that we are hovering
-    appEvents.emit('graph-hover', { pos: pos, panel: panel });
+    appEvents.emit('graph-hover', { pos: pos, panel: this.panel });
   }
 
-  function limitSelection(x2) {
-    x2 = Math.max(x2, yAxisWidth);
-    x2 = Math.min(x2, chartWidth + yAxisWidth);
+  limitSelection(x2) {
+    x2 = Math.max(x2, this.yAxisWidth);
+    x2 = Math.min(x2, this.chartWidth + this.yAxisWidth);
     return x2;
   }
 
-  function drawSelection(posX1, posX2) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+  drawSelection(posX1, posX2) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
       let selectionX = Math.min(posX1, posX2);
       let selectionWidth = Math.abs(posX1 - posX2);
 
       if (selectionWidth > MIN_SELECTION_WIDTH) {
-        heatmap
+        this.heatmap
           .append('rect')
           .attr('class', 'heatmap-selection')
           .attr('x', selectionX)
           .attr('width', selectionWidth)
-          .attr('y', chartTop)
-          .attr('height', chartHeight);
+          .attr('y', this.chartTop)
+          .attr('height', this.chartHeight);
       }
     }
   }
 
-  function clearSelection() {
-    selection.x1 = -1;
-    selection.x2 = -1;
+  clearSelection() {
+    this.selection.x1 = -1;
+    this.selection.x2 = -1;
 
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-selection').remove();
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-selection').remove();
     }
   }
 
-  function drawCrosshair(position) {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  drawCrosshair(position) {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
 
       let posX = position;
-      posX = Math.max(posX, yAxisWidth);
-      posX = Math.min(posX, chartWidth + yAxisWidth);
+      posX = Math.max(posX, this.yAxisWidth);
+      posX = Math.min(posX, this.chartWidth + this.yAxisWidth);
 
-      heatmap
+      this.heatmap
         .append('g')
         .attr('class', 'heatmap-crosshair')
         .attr('transform', 'translate(' + posX + ',0)')
         .append('line')
         .attr('x1', 1)
-        .attr('y1', chartTop)
+        .attr('y1', this.chartTop)
         .attr('x2', 1)
-        .attr('y2', chartBottom)
+        .attr('y2', this.chartBottom)
         .attr('stroke-width', 1);
     }
   }
 
-  function drawSharedCrosshair(pos) {
-    if (heatmap && ctrl.dashboard.graphTooltip !== 0) {
-      let posX = xScale(pos.x) + yAxisWidth;
-      drawCrosshair(posX);
+  drawSharedCrosshair(pos) {
+    if (this.heatmap && this.ctrl.dashboard.graphTooltip !== 0) {
+      let posX = this.xScale(pos.x) + this.yAxisWidth;
+      this.drawCrosshair(posX);
     }
   }
 
-  function clearCrosshair() {
-    if (heatmap) {
-      heatmap.selectAll('.heatmap-crosshair').remove();
+  clearCrosshair() {
+    if (this.heatmap) {
+      this.heatmap.selectAll('.heatmap-crosshair').remove();
     }
   }
 
-  function render() {
-    data = ctrl.data;
-    panel = ctrl.panel;
-    timeRange = ctrl.range;
+  render() {
+    this.data = this.ctrl.data;
+    this.panel = this.ctrl.panel;
+    this.timeRange = this.ctrl.range;
 
-    if (!setElementHeight() || !data) {
+    if (!this.setElementHeight() || !this.data) {
       return;
     }
 
     // Draw default axes and return if no data
-    if (_.isEmpty(data.buckets)) {
-      addHeatmapCanvas();
-      addAxes();
+    if (_.isEmpty(this.data.buckets)) {
+      this.addHeatmapCanvas();
+      this.addAxes();
       return;
     }
 
-    addHeatmap();
-    scope.yAxisWidth = yAxisWidth;
-    scope.xAxisHeight = xAxisHeight;
-    scope.chartHeight = chartHeight;
-    scope.chartWidth = chartWidth;
-    scope.chartTop = chartTop;
+    this.addHeatmap();
+    this.scope.yAxisWidth = this.yAxisWidth;
+    this.scope.xAxisHeight = this.xAxisHeight;
+    this.scope.chartHeight = this.chartHeight;
+    this.scope.chartWidth = this.chartWidth;
+    this.scope.chartTop = this.chartTop;
   }
-
-  // Register selection listeners
-  $heatmap.on('mousedown', onMouseDown);
-  $heatmap.on('mousemove', onMouseMove);
-  $heatmap.on('mouseleave', onMouseLeave);
 }

+ 0 - 320
public/app/plugins/panel/heatmap/specs/renderer_specs.ts

@@ -1,320 +0,0 @@
-import { describe, beforeEach, it, sinon, expect, angularMocks } from '../../../../../test/lib/common';
-
-import '../module';
-import angular from 'angular';
-import $ from 'jquery';
-import helpers from 'test/specs/helpers';
-import TimeSeries from 'app/core/time_series2';
-import moment from 'moment';
-import { Emitter } from 'app/core/core';
-import rendering from '../rendering';
-import { convertToHeatMap, convertToCards, histogramToHeatmap, calculateBucketSize } from '../heatmap_data_converter';
-
-describe('grafanaHeatmap', function() {
-  beforeEach(angularMocks.module('grafana.core'));
-
-  function heatmapScenario(desc, func, elementWidth = 500) {
-    describe(desc, function() {
-      var ctx: any = {};
-
-      ctx.setup = function(setupFunc) {
-        beforeEach(
-          angularMocks.module(function($provide) {
-            $provide.value('timeSrv', new helpers.TimeSrvStub());
-          })
-        );
-
-        beforeEach(
-          angularMocks.inject(function($rootScope, $compile) {
-            var ctrl: any = {
-              colorSchemes: [
-                {
-                  name: 'Oranges',
-                  value: 'interpolateOranges',
-                  invert: 'dark',
-                },
-                { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
-              ],
-              events: new Emitter(),
-              height: 200,
-              panel: {
-                heatmap: {},
-                cards: {
-                  cardPadding: null,
-                  cardRound: null,
-                },
-                color: {
-                  mode: 'spectrum',
-                  cardColor: '#b4ff00',
-                  colorScale: 'linear',
-                  exponent: 0.5,
-                  colorScheme: 'interpolateOranges',
-                  fillBackground: false,
-                },
-                legend: {
-                  show: false,
-                },
-                xBucketSize: 1000,
-                xBucketNumber: null,
-                yBucketSize: 1,
-                yBucketNumber: null,
-                xAxis: {
-                  show: true,
-                },
-                yAxis: {
-                  show: true,
-                  format: 'short',
-                  decimals: null,
-                  logBase: 1,
-                  splitFactor: null,
-                  min: null,
-                  max: null,
-                  removeZeroValues: false,
-                },
-                tooltip: {
-                  show: true,
-                  seriesStat: false,
-                  showHistogram: false,
-                },
-                highlightCards: true,
-              },
-              renderingCompleted: sinon.spy(),
-              hiddenSeries: {},
-              dashboard: {
-                getTimezone: sinon.stub().returns('utc'),
-              },
-              range: {
-                from: moment.utc('01 Mar 2017 10:00:00', 'DD MMM YYYY HH:mm:ss'),
-                to: moment.utc('01 Mar 2017 11:00:00', 'DD MMM YYYY HH:mm:ss'),
-              },
-            };
-
-            var scope = $rootScope.$new();
-            scope.ctrl = ctrl;
-
-            ctx.series = [];
-            ctx.series.push(
-              new TimeSeries({
-                datapoints: [[1, 1422774000000], [2, 1422774060000]],
-                alias: 'series1',
-              })
-            );
-            ctx.series.push(
-              new TimeSeries({
-                datapoints: [[2, 1422774000000], [3, 1422774060000]],
-                alias: 'series2',
-              })
-            );
-
-            ctx.data = {
-              heatmapStats: {
-                min: 1,
-                max: 3,
-                minLog: 1,
-              },
-              xBucketSize: ctrl.panel.xBucketSize,
-              yBucketSize: ctrl.panel.yBucketSize,
-            };
-
-            setupFunc(ctrl, ctx);
-
-            let logBase = ctrl.panel.yAxis.logBase;
-            let bucketsData;
-            if (ctrl.panel.dataFormat === 'tsbuckets') {
-              bucketsData = histogramToHeatmap(ctx.series);
-            } else {
-              bucketsData = convertToHeatMap(ctx.series, ctx.data.yBucketSize, ctx.data.xBucketSize, logBase);
-            }
-            ctx.data.buckets = bucketsData;
-
-            let { cards, cardStats } = convertToCards(bucketsData);
-            ctx.data.cards = cards;
-            ctx.data.cardStats = cardStats;
-
-            let elemHtml = `
-          <div class="heatmap-wrapper">
-            <div class="heatmap-canvas-wrapper">
-              <div class="heatmap-panel" style='width:${elementWidth}px'></div>
-            </div>
-          </div>`;
-
-            var element = angular.element(elemHtml);
-            $compile(element)(scope);
-            scope.$digest();
-
-            ctrl.data = ctx.data;
-            ctx.element = element;
-            rendering(scope, $(element), [], ctrl);
-            ctrl.events.emit('render');
-          })
-        );
-      };
-
-      func(ctx);
-    });
-  }
-
-  heatmapScenario('default options', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '3']);
-    });
-
-    it('should draw correct X axis', function() {
-      var xTicks = getTicks(ctx.element, '.axis-x');
-      let expectedTicks = [
-        formatTime('01 Mar 2017 10:00:00'),
-        formatTime('01 Mar 2017 10:15:00'),
-        formatTime('01 Mar 2017 10:30:00'),
-        formatTime('01 Mar 2017 10:45:00'),
-        formatTime('01 Mar 2017 11:00:00'),
-      ];
-      expect(xTicks).to.eql(expectedTicks);
-    });
-  });
-
-  heatmapScenario('when logBase is 2', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 2;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '4']);
-    });
-  });
-
-  heatmapScenario('when logBase is 10', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.yAxis.logBase = 10;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [20, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 20;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '10', '100']);
-    });
-  });
-
-  heatmapScenario('when logBase is 32', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 32;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[10, 1422774000000], [100, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 100;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '32', '1.0 K']);
-    });
-  });
-
-  heatmapScenario('when logBase is 1024', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1024;
-
-      ctx.series.push(
-        new TimeSeries({
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-          alias: 'series3',
-        })
-      );
-      ctx.data.heatmapStats.max = 300000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '1 K', '1.0 Mil']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "none"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 'none';
-      ctx.data.heatmapStats.max = 10000;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['0', '2000', '4000', '6000', '8000', '10000', '12000']);
-    });
-  });
-
-  heatmapScenario('when Y axis format set to "second"', function(ctx) {
-    ctx.setup(function(ctrl) {
-      ctrl.panel.yAxis.logBase = 1;
-      ctrl.panel.yAxis.format = 's';
-      ctx.data.heatmapStats.max = 3600;
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['0 ns', '17 min', '33 min', '50 min', '1.11 hour']);
-    });
-  });
-
-  heatmapScenario('when data format is Time series buckets', function(ctx) {
-    ctx.setup(function(ctrl, ctx) {
-      ctrl.panel.dataFormat = 'tsbuckets';
-
-      const series = [
-        {
-          alias: '1',
-          datapoints: [[1000, 1422774000000], [200000, 1422774060000]],
-        },
-        {
-          alias: '2',
-          datapoints: [[3000, 1422774000000], [400000, 1422774060000]],
-        },
-        {
-          alias: '3',
-          datapoints: [[2000, 1422774000000], [300000, 1422774060000]],
-        },
-      ];
-      ctx.series = series.map(s => new TimeSeries(s));
-
-      ctx.data.tsBuckets = series.map(s => s.alias).concat('');
-      ctx.data.yBucketSize = 1;
-      let xBucketBoundSet = series[0].datapoints.map(dp => dp[1]);
-      ctx.data.xBucketSize = calculateBucketSize(xBucketBoundSet);
-    });
-
-    it('should draw correct Y axis', function() {
-      var yTicks = getTicks(ctx.element, '.axis-y');
-      expect(yTicks).to.eql(['1', '2', '3', '']);
-    });
-  });
-});
-
-function getTicks(element, axisSelector) {
-  return element
-    .find(axisSelector)
-    .find('text')
-    .map(function() {
-      return this.textContent;
-    })
-    .get();
-}
-
-function formatTime(timeStr) {
-  let format = 'HH:mm';
-  return moment.utc(timeStr, 'DD MMM YYYY HH:mm:ss').format(format);
-}

+ 1 - 1
public/app/plugins/panel/table/module.html

@@ -5,7 +5,7 @@
 		<table class="table-panel-table">
 			<thead>
 				<tr>
-					<th ng-repeat="col in ctrl.table.columns" ng-hide="col.hidden">
+					<th ng-repeat="col in ctrl.table.columns" ng-if="!col.hidden">
 						<div class="table-panel-table-header-inner pointer" ng-click="ctrl.toggleColumnSort(col, $index)">
 							{{col.title}}
 							<span class="table-panel-table-header-controls" ng-if="col.sort">

+ 4 - 0
public/app/plugins/panel/table/renderer.ts

@@ -238,6 +238,10 @@ export class TableRenderer {
       column.hidden = false;
     }
 
+    if (column.hidden === true) {
+      return '';
+    }
+
     if (column.style && column.style.preserveFormat) {
       cellClasses.push('table-panel-cell-pre');
     }

+ 2 - 0
public/app/routes/dashboard_loaders.ts

@@ -34,7 +34,9 @@ export class LoadDashboardCtrl {
         const url = locationUtil.stripBaseFromUrl(result.meta.url);
 
         if (url !== $location.path()) {
+          // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times.
           $location.path(url).replace();
+          return;
         }
       }
 

+ 4 - 4
public/views/index.template.html

@@ -11,7 +11,7 @@
 
   <base href="[[.AppSubUrl]]/" />
 
-  <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]">
+  <link rel="stylesheet" href="public/build/grafana.[[ .Theme ]].css?v[[ .BuildVersion ]]+[[ .BuildCommit ]]">
 
   <link rel="icon" type="image/png" href="public/img/fav32.png">
   <link rel="mask-icon" href="public/img/grafana_mask_icon.svg" color="#F05A28">
@@ -107,12 +107,12 @@
     <iframe src="//www.googletagmanager.com/ns.html?id=[[.GoogleTagManagerId]]" height="0" width="0" style="display:none;visibility:hidden"></iframe>
   </noscript>
   <script>(function (w, d, s, l, i) {
-    w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],
-      j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
+      w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' }); var f = d.getElementsByTagName(s)[0],
+        j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src = '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
     })(window, document, 'script', 'dataLayer', '[[.GoogleTagManagerId]]');</script>
   <!-- End Google Tag Manager -->
   [[end]]
 
 </body>
 
-</html>
+</html>

+ 0 - 4
scripts/grunt/default_task.js

@@ -9,8 +9,6 @@ module.exports = function(grunt) {
   ]);
 
   grunt.registerTask('test', [
-    'jscs',
-    'jshint',
     'sasslint',
     'exec:tslint',
     "exec:jest",
@@ -19,8 +17,6 @@ module.exports = function(grunt) {
   ]);
 
   grunt.registerTask('precommit', [
-    'jscs',
-    'jshint',
     'sasslint',
     'exec:tslint',
     'no-only-tests'

+ 0 - 22
scripts/grunt/options/jscs.js

@@ -1,22 +0,0 @@
-module.exports = function(config) {
-  return {
-    src: [
-      'Gruntfile.js',
-      '<%= srcDir %>/app/**/*.js',
-      '<%= srcDir %>/plugin/**/*.js',
-      '!<%= srcDir %>/app/dashboards/*'
-    ],
-    options: {
-      config: ".jscs.json",
-    },
-  };
-};
-
-/*
- "requireCurlyBraces": ["if", "else", "for", "while", "do", "try", "catch"],
-    "requireSpaceAfterKeywords": ["if", "else", "for", "while", "do", "switch", "return", "try", "catch"],
-    "disallowLeftStickedOperators": ["?", "+", "-", "/", "*", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
-    "disallowRightStickedOperators": ["?", "+", "/", "*", ":", "=", "==", "===", "!=", "!==", ">", ">=", "<", "<="],
-    "requireRightStickedOperators": ["!"],
-    "requireLeftStickedOperators": [","],
-   */

+ 0 - 20
scripts/grunt/options/jshint.js

@@ -1,20 +0,0 @@
-module.exports = function(config) {
-  return {
-    source: {
-      files: {
-        src: ['Gruntfile.js', '<%= srcDir %>/app/**/*.js'],
-      }
-    },
-    options: {
-      jshintrc: true,
-      reporter: require('jshint-stylish'),
-      ignores: [
-        'node_modules/*',
-        'dist/*',
-        'sample/*',
-        '<%= srcDir %>/vendor/*',
-        '<%= srcDir %>/app/dashboards/*'
-      ]
-    }
-  };
-};

+ 0 - 45
tasks/options/copy.js

@@ -1,45 +0,0 @@
-module.exports = function(config) {
-  return {
-    // copy source to temp, we will minify in place for the dist build
-    everything_but_less_to_temp: {
-      cwd: '<%= srcDir %>',
-      expand: true,
-      src: ['**/*', '!**/*.less'],
-      dest: '<%= tempDir %>'
-    },
-
-    public_to_gen: {
-      cwd: '<%= srcDir %>',
-      expand: true,
-      src: ['**/*', '!**/*.less'],
-      dest: '<%= genDir %>'
-    },
-
-    node_modules: {
-      cwd: './node_modules',
-      expand: true,
-      src: [
-        'ace-builds/src-noconflict/**/*',
-        'eventemitter3/*.js',
-        'systemjs/dist/*.js',
-        'es6-promise/**/*',
-        'es6-shim/*.js',
-        'reflect-metadata/*.js',
-        'reflect-metadata/*.ts',
-        'reflect-metadata/*.d.ts',
-        'rxjs/**/*',
-        'tether/**/*',
-        'tether-drop/**/*',
-        'tether-drop/**/*',
-        'remarkable/dist/*',
-        'remarkable/dist/*',
-        'virtual-scroll/**/*',
-        'mousetrap/**/*',
-        'twemoji/2/twemoji.amd*',
-        'twemoji/2/svg/*.svg',
-      ],
-      dest: '<%= srcDir %>/vendor/npm'
-    }
-
-  };
-};

+ 28 - 340
yarn.lock

@@ -414,10 +414,6 @@ JSONStream@^1.3.2:
     jsonparse "^1.2.0"
     through ">=2.2.7 <3"
 
-JSV@^4.0.x:
-  version "4.0.2"
-  resolved "https://registry.yarnpkg.com/JSV/-/JSV-4.0.2.tgz#d077f6825571f82132f9dffaed587b4029feff57"
-
 abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -869,10 +865,6 @@ async-limiter@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/async-limiter/-/async-limiter-1.0.0.tgz#78faed8c3d074ab81f22b4e985d79e8738f720f8"
 
-async@0.2.x, async@~0.2.6, async@~0.2.9:
-  version "0.2.10"
-  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
-
 async@^1.4.0, async@^1.5.0, async@^1.5.2, async@~1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
@@ -883,6 +875,10 @@ async@^2.0.0, async@^2.1.4, async@^2.4.1, async@^2.6.0:
   dependencies:
     lodash "^4.17.10"
 
+async@~0.2.6:
+  version "0.2.10"
+  resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
+
 asynckit@^0.4.0:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
@@ -1564,7 +1560,7 @@ babel-types@^6.18.0, babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26
     lodash "^4.17.4"
     to-fast-properties "^1.0.3"
 
-babylon@^6.17.3, babylon@^6.18.0, babylon@^6.8.1:
+babylon@^6.17.3, babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
 
@@ -1626,10 +1622,6 @@ bcrypt-pbkdf@^1.0.0:
   dependencies:
     tweetnacl "^0.14.3"
 
-beeper@^1.1.0:
-  version "1.1.1"
-  resolved "https://registry.yarnpkg.com/beeper/-/beeper-1.1.1.tgz#e6d5ea8c5dad001304a70b22638447f69cb2f809"
-
 better-assert@~1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
@@ -2109,7 +2101,7 @@ center-align@^0.1.1:
     align-text "^0.1.3"
     lazy-cache "^1.0.3"
 
-chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.0, chalk@~1.1.1:
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -2315,7 +2307,7 @@ cli-table2@^0.2.0, cli-table2@~0.2.0:
   optionalDependencies:
     colors "^1.1.2"
 
-cli-table@^0.3.1, cli-table@~0.3.1:
+cli-table@^0.3.1:
   version "0.3.1"
   resolved "https://registry.yarnpkg.com/cli-table/-/cli-table-0.3.1.tgz#f53b05266a8b1a0b934b3d0821e6e2dc5914ae23"
   dependencies:
@@ -2332,13 +2324,6 @@ cli-width@^2.0.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639"
 
-cli@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/cli/-/cli-1.0.1.tgz#22817534f24bfa4950c34d532d48ecbc621b8c14"
-  dependencies:
-    exit "0.1.2"
-    glob "^7.1.1"
-
 clipboard@^1.7.1:
   version "1.7.1"
   resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-1.7.1.tgz#360d6d6946e99a7a1fef395e42ba92b5e9b5a16b"
@@ -2490,10 +2475,6 @@ colors@0.5.x:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/colors/-/colors-0.5.1.tgz#7d0023eaeb154e8ee9fce75dcb923d0ed1667774"
 
-colors@0.6.x:
-  version "0.6.2"
-  resolved "https://registry.yarnpkg.com/colors/-/colors-0.6.2.tgz#2423fe6678ac0c5dae8852e5d0e5be08c997abcc"
-
 colors@1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
@@ -2539,7 +2520,7 @@ commander@2.8.x:
   dependencies:
     graceful-readlink ">= 1.0.0"
 
-commander@2.9.x, commander@~2.9.0:
+commander@2.9.x:
   version "2.9.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4"
   dependencies:
@@ -2549,12 +2530,6 @@ commander@~2.13.0:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
 
-comment-parser@^0.3.1:
-  version "0.3.2"
-  resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-0.3.2.tgz#3c03f0776b86a36dfd9a0a2c97c6307f332082fe"
-  dependencies:
-    readable-stream "^2.0.4"
-
 commondir@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b"
@@ -2660,7 +2635,7 @@ connect@^3.6.0:
     parseurl "~1.3.2"
     utils-merge "1.0.1"
 
-console-browserify@1.1.x, console-browserify@^1.1.0:
+console-browserify@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/console-browserify/-/console-browserify-1.1.0.tgz#f0241c45730a9fc6323b206dbf38edc741d0bb10"
   dependencies:
@@ -2978,14 +2953,6 @@ csstype@^2.2.0:
   version "2.5.3"
   resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.5.3.tgz#2504152e6e1cc59b32098b7f5d6a63f16294c1f7"
 
-cst@^0.4.3:
-  version "0.4.10"
-  resolved "https://registry.yarnpkg.com/cst/-/cst-0.4.10.tgz#9c05c825290a762f0a85c0aabb8c0fe035ae8516"
-  dependencies:
-    babel-runtime "^6.9.2"
-    babylon "^6.8.1"
-    source-map-support "^0.4.0"
-
 currently-unhandled@^0.4.1:
   version "0.4.1"
   resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea"
@@ -2996,10 +2963,6 @@ custom-event@~1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
 
-cycle@1.0.x:
-  version "1.0.3"
-  resolved "https://registry.yarnpkg.com/cycle/-/cycle-1.0.3.tgz#21e80b2be8580f98b468f379430662b046c34ad2"
-
 cyclist@~0.2.2:
   version "0.2.2"
   resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-0.2.2.tgz#1b33792e11e914a2fd6d6ed6447464444e5fa640"
@@ -3324,7 +3287,7 @@ dedent@^0.7.0:
   version "0.7.0"
   resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c"
 
-deep-equal@*, deep-equal@^1.0.1:
+deep-equal@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
 
@@ -3604,12 +3567,6 @@ domhandler@2.1:
   dependencies:
     domelementtype "1"
 
-domhandler@2.3:
-  version "2.3.0"
-  resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.3.0.tgz#2de59a0822d5027fabff6f032c2b25a2a8abe738"
-  dependencies:
-    domelementtype "1"
-
 domhandler@^2.3.0:
   version "2.4.2"
   resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803"
@@ -3622,7 +3579,7 @@ domutils@1.1:
   dependencies:
     domelementtype "1"
 
-domutils@1.5, domutils@1.5.1:
+domutils@1.5.1:
   version "1.5.1"
   resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
   dependencies:
@@ -3813,10 +3770,6 @@ ent@~2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/ent/-/ent-2.2.0.tgz#e964219325a21d05f44466a2f686ed6ce5f5dd1d"
 
-entities@1.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/entities/-/entities-1.0.0.tgz#b2987aa3821347fcde642b24fdfc9e4fb712bf26"
-
 entities@^1.1.1, entities@~1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0"
@@ -4181,7 +4134,7 @@ exit-hook@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8"
 
-exit@0.1.2, exit@0.1.x, exit@^0.1.2, exit@~0.1.1, exit@~0.1.2:
+exit@^0.1.2, exit@~0.1.1:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c"
 
@@ -4362,10 +4315,6 @@ extsprintf@^1.2.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f"
 
-eyes@0.1.x:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/eyes/-/eyes-0.1.8.tgz#62cf120234c683785d902348a800ef3e0cc20bc0"
-
 fast-deep-equal@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614"
@@ -4945,9 +4894,9 @@ glob@7.1.2, glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glo
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^5.0.1, glob@~5.0.0:
-  version "5.0.15"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+glob@^6.0.4:
+  version "6.0.4"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
   dependencies:
     inflight "^1.0.4"
     inherits "2"
@@ -4955,9 +4904,9 @@ glob@^5.0.1, glob@~5.0.0:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
-glob@^6.0.4:
-  version "6.0.4"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22"
+glob@~5.0.0:
+  version "5.0.15"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
   dependencies:
     inflight "^1.0.4"
     inherits "2"
@@ -5199,27 +5148,10 @@ grunt-contrib-cssmin@~1.0.2:
     clean-css "~3.4.2"
     maxmin "^1.1.0"
 
-grunt-contrib-jshint@~1.1.0:
-  version "1.1.0"
-  resolved "https://registry.yarnpkg.com/grunt-contrib-jshint/-/grunt-contrib-jshint-1.1.0.tgz#369d909b2593c40e8be79940b21340850c7939ac"
-  dependencies:
-    chalk "^1.1.1"
-    hooker "^0.2.3"
-    jshint "~2.9.4"
-
 grunt-exec@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/grunt-exec/-/grunt-exec-1.0.1.tgz#e5d53a39c5f346901305edee5c87db0f2af999c4"
 
-grunt-jscs@3.0.1:
-  version "3.0.1"
-  resolved "https://registry.yarnpkg.com/grunt-jscs/-/grunt-jscs-3.0.1.tgz#1fae50e3e955df9e3a9d9425aec22accae008092"
-  dependencies:
-    hooker "~0.2.3"
-    jscs "~3.0.5"
-    lodash "~4.6.1"
-    vow "~0.4.1"
-
 grunt-karma@~2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/grunt-karma/-/grunt-karma-2.0.0.tgz#753583d115dfdc055fe57e58f96d6b3c7e612118"
@@ -5530,7 +5462,7 @@ homedir-polyfill@^1.0.1:
   dependencies:
     parse-passwd "^1.0.0"
 
-hooker@^0.2.3, hooker@~0.2.3:
+hooker@~0.2.3:
   version "0.2.3"
   resolved "https://registry.yarnpkg.com/hooker/-/hooker-0.2.3.tgz#b834f723cc4a242aa65963459df6d984c5d3d959"
 
@@ -5614,16 +5546,6 @@ html-webpack-plugin@^3.2.0:
     toposort "^1.0.0"
     util.promisify "1.0.0"
 
-htmlparser2@3.8.3, htmlparser2@3.8.x:
-  version "3.8.3"
-  resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.8.3.tgz#996c28b191516a8be86501a7d79757e5c70c1068"
-  dependencies:
-    domelementtype "1"
-    domhandler "2.3"
-    domutils "1.5"
-    entities "1.0"
-    readable-stream "1.1"
-
 htmlparser2@^3.9.1:
   version "3.9.2"
   resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@@ -5739,10 +5661,6 @@ husky@^0.14.3:
     normalize-path "^1.0.0"
     strip-indent "^2.0.0"
 
-i@0.3.x:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/i/-/i-0.3.6.tgz#d96c92732076f072711b6b10fd7d4f65ad8ee23d"
-
 iconv-lite@0.4, iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
   version "0.4.23"
   resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
@@ -5838,10 +5756,6 @@ inflight@^1.0.4, inflight@~1.0.6:
     once "^1.3.0"
     wrappy "1"
 
-inherit@^2.2.2:
-  version "2.2.6"
-  resolved "https://registry.yarnpkg.com/inherit/-/inherit-2.2.6.tgz#f1614b06c8544e8128e4229c86347db73ad9788d"
-
 inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
@@ -5938,10 +5852,6 @@ ipaddr.js@1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b"
 
-irregular-plurals@^1.0.0:
-  version "1.4.0"
-  resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766"
-
 is-absolute-url@^2.0.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-2.1.0.tgz#50530dfb84fcc9aa7dbe7852e83a37b93b9f2aa6"
@@ -6348,7 +6258,7 @@ isomorphic-fetch@^2.1.1:
     node-fetch "^1.0.1"
     whatwg-fetch ">=0.10.0"
 
-isstream@0.1.x, isstream@~0.1.2:
+isstream@~0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
 
@@ -6748,14 +6658,6 @@ js-yaml@^3.4.3, js-yaml@^3.4.6, js-yaml@^3.5.1, js-yaml@^3.5.4, js-yaml@^3.7.0,
     argparse "^1.0.7"
     esprima "^4.0.0"
 
-js-yaml@~3.4.0:
-  version "3.4.6"
-  resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.4.6.tgz#6be1b23f6249f53d293370fd4d1aaa63ce1b4eb0"
-  dependencies:
-    argparse "^1.0.2"
-    esprima "^2.6.0"
-    inherit "^2.2.2"
-
 js-yaml@~3.5.2:
   version "3.5.5"
   resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.5.5.tgz#0377c38017cabc7322b0d1fbcd25a491641f2fbe"
@@ -6814,54 +6716,6 @@ jscodeshift@^0.5.0:
     temp "^0.8.1"
     write-file-atomic "^1.2.0"
 
-jscs-jsdoc@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/jscs-jsdoc/-/jscs-jsdoc-2.0.0.tgz#f53ebce029aa3125bd88290ba50d64d4510a4871"
-  dependencies:
-    comment-parser "^0.3.1"
-    jsdoctypeparser "~1.2.0"
-
-jscs-preset-wikimedia@~1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/jscs-preset-wikimedia/-/jscs-preset-wikimedia-1.0.1.tgz#a6a5fa5967fd67a5d609038e1c794eaf41d4233d"
-
-jscs@~3.0.5:
-  version "3.0.7"
-  resolved "https://registry.yarnpkg.com/jscs/-/jscs-3.0.7.tgz#7141b4dff5b86e32d0e99d764b836767c30d201a"
-  dependencies:
-    chalk "~1.1.0"
-    cli-table "~0.3.1"
-    commander "~2.9.0"
-    cst "^0.4.3"
-    estraverse "^4.1.0"
-    exit "~0.1.2"
-    glob "^5.0.1"
-    htmlparser2 "3.8.3"
-    js-yaml "~3.4.0"
-    jscs-jsdoc "^2.0.0"
-    jscs-preset-wikimedia "~1.0.0"
-    jsonlint "~1.6.2"
-    lodash "~3.10.0"
-    minimatch "~3.0.0"
-    natural-compare "~1.2.2"
-    pathval "~0.1.1"
-    prompt "~0.2.14"
-    reserved-words "^0.1.1"
-    resolve "^1.1.6"
-    strip-bom "^2.0.0"
-    strip-json-comments "~1.0.2"
-    to-double-quotes "^2.0.0"
-    to-single-quotes "^2.0.0"
-    vow "~0.4.8"
-    vow-fs "~0.3.4"
-    xmlbuilder "^3.1.0"
-
-jsdoctypeparser@~1.2.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/jsdoctypeparser/-/jsdoctypeparser-1.2.0.tgz#e7dedc153a11849ffc5141144ae86a7ef0c25392"
-  dependencies:
-    lodash "^3.7.0"
-
 jsdom@^11.5.1:
   version "11.11.0"
   resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-11.11.0.tgz#df486efad41aee96c59ad7a190e2449c7eb1110e"
@@ -6901,30 +6755,6 @@ jsesc@^1.3.0:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b"
 
-jshint-stylish@~2.2.1:
-  version "2.2.1"
-  resolved "https://registry.yarnpkg.com/jshint-stylish/-/jshint-stylish-2.2.1.tgz#242082a2c035ae03fd81044e0570cc4208cf6e61"
-  dependencies:
-    beeper "^1.1.0"
-    chalk "^1.0.0"
-    log-symbols "^1.0.0"
-    plur "^2.1.0"
-    string-length "^1.0.0"
-    text-table "^0.2.0"
-
-jshint@~2.9.4:
-  version "2.9.5"
-  resolved "https://registry.yarnpkg.com/jshint/-/jshint-2.9.5.tgz#1e7252915ce681b40827ee14248c46d34e9aa62c"
-  dependencies:
-    cli "~1.0.0"
-    console-browserify "1.1.x"
-    exit "0.1.x"
-    htmlparser2 "3.8.x"
-    lodash "3.7.x"
-    minimatch "~3.0.2"
-    shelljs "0.3.x"
-    strip-json-comments "1.0.x"
-
 json-buffer@3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.0.tgz#5b1f397afc75d677bde8bcfc0e47e1f9a3d9a898"
@@ -6981,13 +6811,6 @@ jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
 
-jsonlint@~1.6.2:
-  version "1.6.3"
-  resolved "https://registry.yarnpkg.com/jsonlint/-/jsonlint-1.6.3.tgz#cb5e31efc0b78291d0d862fbef05900adf212988"
-  dependencies:
-    JSV "^4.0.x"
-    nomnom "^1.5.x"
-
 jsonparse@^1.2.0:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
@@ -7497,11 +7320,7 @@ lodash.without@~4.4.0:
   version "4.4.0"
   resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac"
 
-lodash@3.7.x:
-  version "3.7.0"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.7.0.tgz#3678bd8ab995057c07ade836ed2ef087da811d45"
-
-lodash@^3.10.1, lodash@^3.5.0, lodash@^3.6.0, lodash@^3.7.0, lodash@^3.8.0, lodash@~3.10.0:
+lodash@^3.10.1, lodash@^3.6.0, lodash@^3.8.0:
   version "3.10.1"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
 
@@ -7513,11 +7332,7 @@ lodash@~4.3.0:
   version "4.3.0"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.3.0.tgz#efd9c4a6ec53f3b05412429915c3e4824e4d25a4"
 
-lodash@~4.6.1:
-  version "4.6.1"
-  resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.6.1.tgz#df00c1164ad236b183cfc3887a5e8d38cc63cbbc"
-
-log-symbols@^1.0.0, log-symbols@^1.0.2:
+log-symbols@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18"
   dependencies:
@@ -7990,7 +7805,7 @@ mixin-object@^2.0.1:
     for-in "^0.1.3"
     is-extendable "^0.1.1"
 
-mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
+mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
   dependencies:
@@ -8121,20 +7936,12 @@ natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
 
-natural-compare@~1.2.2:
-  version "1.2.2"
-  resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.2.2.tgz#1f96d60e3141cac1b6d05653ce0daeac763af6aa"
-
 ncname@1.0.x:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/ncname/-/ncname-1.0.0.tgz#5b57ad18b1ca092864ef62b0b1ed8194f383b71c"
   dependencies:
     xml-char-classes "^1.0.0"
 
-ncp@0.4.x:
-  version "0.4.2"
-  resolved "https://registry.yarnpkg.com/ncp/-/ncp-0.4.2.tgz#abcc6cbd3ec2ed2a729ff6e7c1fa8f01784a8574"
-
 nearley@^2.7.10:
   version "2.13.0"
   resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.13.0.tgz#6e7b0f4e68bfc3e74c99eaef2eda39e513143439"
@@ -8359,7 +8166,7 @@ node-sass@^4.7.2:
     stdout-stream "^1.4.0"
     "true-case-path" "^1.0.2"
 
-nomnom@^1.5.x, nomnom@^1.8.1:
+nomnom@^1.8.1:
   version "1.8.1"
   resolved "https://registry.yarnpkg.com/nomnom/-/nomnom-1.8.1.tgz#2151f722472ba79e50a76fc125bb8c8f2e4dc2a7"
   dependencies:
@@ -9207,10 +9014,6 @@ path-type@^3.0.0:
   dependencies:
     pify "^3.0.0"
 
-pathval@~0.1.1:
-  version "0.1.1"
-  resolved "https://registry.yarnpkg.com/pathval/-/pathval-0.1.1.tgz#08f911cdca9cce5942880da7817bc0b723b66d82"
-
 pbkdf2@^3.0.3:
   version "3.0.16"
   resolved "https://registry.yarnpkg.com/pbkdf2/-/pbkdf2-3.0.16.tgz#7404208ec6b01b62d85bf83853a8064f8d9c2a5c"
@@ -9273,20 +9076,6 @@ pkg-up@^1.0.0:
   dependencies:
     find-up "^1.0.0"
 
-pkginfo@0.3.x:
-  version "0.3.1"
-  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.3.1.tgz#5b29f6a81f70717142e09e765bbeab97b4f81e21"
-
-pkginfo@0.x.x:
-  version "0.4.1"
-  resolved "https://registry.yarnpkg.com/pkginfo/-/pkginfo-0.4.1.tgz#b5418ef0439de5425fc4995042dced14fb2a84ff"
-
-plur@^2.1.0:
-  version "2.1.2"
-  resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a"
-  dependencies:
-    irregular-plurals "^1.0.0"
-
 pluralize@^1.2.1:
   version "1.2.1"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45"
@@ -9810,16 +9599,6 @@ promise@^7.1.1:
   dependencies:
     asap "~2.0.3"
 
-prompt@~0.2.14:
-  version "0.2.14"
-  resolved "https://registry.yarnpkg.com/prompt/-/prompt-0.2.14.tgz#57754f64f543fd7b0845707c818ece618f05ffdc"
-  dependencies:
-    pkginfo "0.x.x"
-    read "1.0.x"
-    revalidator "0.1.x"
-    utile "0.2.x"
-    winston "0.8.x"
-
 promzard@^0.3.0:
   version "0.3.0"
   resolved "https://registry.yarnpkg.com/promzard/-/promzard-0.3.0.tgz#26a5d6ee8c7dee4cb12208305acfb93ba382a9ee"
@@ -10304,7 +10083,7 @@ read-pkg@^3.0.0:
     normalize-package-data "^2.3.2"
     path-type "^3.0.0"
 
-read@1, read@1.0.x, read@~1.0.1, read@~1.0.7:
+read@1, read@~1.0.1, read@~1.0.7:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
   dependencies:
@@ -10331,15 +10110,6 @@ readable-stream@1.0, readable-stream@~1.0.2:
     isarray "0.0.1"
     string_decoder "~0.10.x"
 
-readable-stream@1.1:
-  version "1.1.13"
-  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.13.tgz#f6eef764f514c89e2b9e23146a75ba106756d23e"
-  dependencies:
-    core-util-is "~1.0.0"
-    inherits "~2.0.1"
-    isarray "0.0.1"
-    string_decoder "~0.10.x"
-
 readable-stream@~1.1.10:
   version "1.1.14"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
@@ -10660,10 +10430,6 @@ requires-port@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
 
-reserved-words@^0.1.1:
-  version "0.1.2"
-  resolved "https://registry.yarnpkg.com/reserved-words/-/reserved-words-0.1.2.tgz#00a0940f98cd501aeaaac316411d9adc52b31ab1"
-
 resolve-cwd@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-2.0.0.tgz#00a9f7387556e27038eae232caa372a6a59b665a"
@@ -10754,17 +10520,13 @@ retry@^0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"
 
-revalidator@0.1.x:
-  version "0.1.8"
-  resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b"
-
 right-align@^0.1.1:
   version "0.1.3"
   resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef"
   dependencies:
     align-text "^0.1.1"
 
-rimraf@2, rimraf@2.x.x, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@~2.6.2:
   version "2.6.2"
   resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
   dependencies:
@@ -11116,10 +10878,6 @@ shell-quote@^1.6.1:
     array-reduce "~0.0.0"
     jsonify "~0.0.0"
 
-shelljs@0.3.x:
-  version "0.3.0"
-  resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.3.0.tgz#3596e6307a781544f591f37da618360f31db57b1"
-
 shelljs@^0.6.0:
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.6.1.tgz#ec6211bed1920442088fe0f70b2837232ed2c8a8"
@@ -11432,7 +11190,7 @@ source-map-resolve@^0.5.0:
     source-map-url "^0.4.0"
     urix "^0.1.0"
 
-source-map-support@^0.4.0, source-map-support@^0.4.15:
+source-map-support@^0.4.15:
   version "0.4.18"
   resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f"
   dependencies:
@@ -11555,10 +11313,6 @@ stack-parser@^0.0.1:
   version "0.0.1"
   resolved "https://registry.yarnpkg.com/stack-parser/-/stack-parser-0.0.1.tgz#7d3b63a17887e9e2c2bf55dbd3318fe34a39d1e7"
 
-stack-trace@0.0.x:
-  version "0.0.10"
-  resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
-
 stack-utils@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.1.tgz#d4f33ab54e8e38778b0ca5cfd3b3afb12db68620"
@@ -11649,12 +11403,6 @@ strict-uri-encode@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
 
-string-length@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/string-length/-/string-length-1.0.1.tgz#56970fb1c38558e9e70b728bf3de269ac45adfac"
-  dependencies:
-    strip-ansi "^3.0.0"
-
 string-length@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed"
@@ -11766,7 +11514,7 @@ strip-indent@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68"
 
-strip-json-comments@1.0.x, strip-json-comments@~1.0.1, strip-json-comments@~1.0.2:
+strip-json-comments@~1.0.1:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-1.0.4.tgz#1e15fbcac97d3ee99bf2d73b4c656b082bbafb91"
 
@@ -12029,10 +11777,6 @@ to-buffer@^1.1.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
 
-to-double-quotes@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/to-double-quotes/-/to-double-quotes-2.0.0.tgz#aaf231d6fa948949f819301bbab4484d8588e4a7"
-
 to-fast-properties@^1.0.3:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
@@ -12059,10 +11803,6 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
-to-single-quotes@^2.0.0:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/to-single-quotes/-/to-single-quotes-2.0.1.tgz#7cc29151f0f5f2c41946f119f5932fe554170125"
-
 toposort@^1.0.0:
   version "1.0.7"
   resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029"
@@ -12527,25 +12267,10 @@ utila@~0.4:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
 
-utile@0.2.x:
-  version "0.2.1"
-  resolved "https://registry.yarnpkg.com/utile/-/utile-0.2.1.tgz#930c88e99098d6220834c356cbd9a770522d90d7"
-  dependencies:
-    async "~0.2.9"
-    deep-equal "*"
-    i "0.3.x"
-    mkdirp "0.x.x"
-    ncp "0.4.x"
-    rimraf "2.x.x"
-
 utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
-uuid@^2.0.2:
-  version "2.0.3"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
-
 uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
@@ -12623,25 +12348,6 @@ void-elements@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-2.0.1.tgz#c066afb582bb1cb4128d60ea92392e94d5e9dbec"
 
-vow-fs@~0.3.4:
-  version "0.3.6"
-  resolved "https://registry.yarnpkg.com/vow-fs/-/vow-fs-0.3.6.tgz#2d4c59be22e2bf2618ddf597ab4baa923be7200d"
-  dependencies:
-    glob "^7.0.5"
-    uuid "^2.0.2"
-    vow "^0.4.7"
-    vow-queue "^0.4.1"
-
-vow-queue@^0.4.1:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/vow-queue/-/vow-queue-0.4.3.tgz#4ba8f64b56e9212c0dbe57f1405aeebd54cce78d"
-  dependencies:
-    vow "^0.4.17"
-
-vow@^0.4.17, vow@^0.4.7, vow@~0.4.1, vow@~0.4.8:
-  version "0.4.17"
-  resolved "https://registry.yarnpkg.com/vow/-/vow-0.4.17.tgz#b16e08fae58c52f3ebc6875f2441b26a92682904"
-
 vue-parser@^1.1.5:
   version "1.1.6"
   resolved "https://registry.yarnpkg.com/vue-parser/-/vue-parser-1.1.6.tgz#3063c8431795664ebe429c23b5506899706e6355"
@@ -12960,18 +12666,6 @@ window-size@0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
 
-winston@0.8.x:
-  version "0.8.3"
-  resolved "https://registry.yarnpkg.com/winston/-/winston-0.8.3.tgz#64b6abf4cd01adcaefd5009393b1d8e8bec19db0"
-  dependencies:
-    async "0.2.x"
-    colors "0.6.x"
-    cycle "1.0.x"
-    eyes "0.1.x"
-    isstream "0.1.x"
-    pkginfo "0.3.x"
-    stack-trace "0.0.x"
-
 wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
@@ -13053,12 +12747,6 @@ xml-name-validator@^3.0.0:
   version "3.0.0"
   resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
 
-xmlbuilder@^3.1.0:
-  version "3.1.0"
-  resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-3.1.0.tgz#2c86888f2d4eade850fa38ca7f7223f7209516e1"
-  dependencies:
-    lodash "^3.5.0"
-
 xmlhttprequest-ssl@1.5.3:
   version "1.5.3"
   resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.3.tgz#185a888c04eca46c3e4070d99f7b49de3528992d"