Browse Source

Merge branch 'master' into develop

Torkel Ödegaard 8 năm trước cách đây
mục cha
commit
fff0fa2aee
85 tập tin đã thay đổi với 4025 bổ sung3274 xóa
  1. 2 0
      .gitignore
  2. 18 1
      CHANGELOG.md
  3. 2 1
      Gruntfile.js
  4. 17 3
      README.md
  5. 7 7
      docs/sources/features/datasources/postgres.md
  6. 31 0
      docs/sources/http_api/annotations.md
  7. 14 0
      docs/sources/project/building_from_source.md
  8. 23 0
      jest.config.js
  9. 8 4
      package.json
  10. 1 1
      packaging/deb/init.d/grafana-server
  11. 1 1
      packaging/rpm/init.d/grafana-server
  12. 35 14
      pkg/api/annotations.go
  13. 2 0
      pkg/models/datasource.go
  14. 1 1
      pkg/services/alerting/notifiers/sensu.go
  15. 2 2
      pkg/tsdb/postgres/macros.go
  16. 2 2
      pkg/tsdb/postgres/macros_test.go
  17. 8 0
      public/app/core/angular_wrappers.ts
  18. 4 3
      public/app/core/core.ts
  19. 2 0
      public/app/core/directives/misc.js
  20. 1 2
      public/app/core/directives/ng_model_on_blur.js
  21. 4 5
      public/app/core/specs/PasswordStrength.jest.tsx
  22. 19 19
      public/app/core/specs/datemath.jest.ts
  23. 6 8
      public/app/core/specs/emitter.jest.ts
  24. 3 5
      public/app/core/specs/flatten.jest.ts
  25. 347 0
      public/app/core/specs/kbn.jest.ts
  26. 22 24
      public/app/core/specs/rangeutil.jest.ts
  27. 8 10
      public/app/core/specs/table_model.jest.ts
  28. 41 42
      public/app/core/specs/time_series.jest.ts
  29. 0 7
      public/app/core/time_series.js
  30. 0 2
      public/app/core/ui/PasswordStrength.tsx
  31. 0 936
      public/app/core/utils/kbn.js
  32. 968 0
      public/app/core/utils/kbn.ts
  33. 2 0
      public/app/features/dashboard/dashboardLoaderSrv.js
  34. 5 7
      public/app/features/dashboard/export/export_modal.ts
  35. 2 0
      public/app/features/panellinks/linkSrv.js
  36. 0 0
      public/app/features/plugins/built_in_plugins.ts
  37. 21 4
      public/app/features/plugins/plugin_loader.ts
  38. 0 2
      public/app/features/templating/adhoc_variable.ts
  39. 12 8
      public/app/features/templating/all.ts
  40. 0 2
      public/app/features/templating/query_variable.ts
  41. 10 12
      public/app/features/templating/specs/adhoc_variable.jest.ts
  42. 22 24
      public/app/features/templating/specs/query_variable.jest.ts
  43. 44 52
      public/app/features/templating/specs/template_srv.jest.ts
  44. 0 253
      public/app/features/templating/templateSrv.js
  45. 244 0
      public/app/features/templating/template_srv.ts
  46. 1 3
      public/app/features/templating/variable.ts
  47. 3 0
      public/app/plugins/datasource/cloudwatch/datasource.js
  48. 6 0
      public/app/plugins/datasource/elasticsearch/datasource.ts
  49. 2 0
      public/app/plugins/datasource/graphite/add_graphite_func.js
  50. 0 2
      public/app/plugins/datasource/graphite/gfunc.d.ts
  51. 0 986
      public/app/plugins/datasource/graphite/gfunc.js
  52. 1008 0
      public/app/plugins/datasource/graphite/gfunc.ts
  53. 26 28
      public/app/plugins/datasource/graphite/specs/gfunc.jest.ts
  54. 50 52
      public/app/plugins/datasource/graphite/specs/lexer.jest.ts
  55. 182 0
      public/app/plugins/datasource/graphite/specs/parser.jest.ts
  56. 0 183
      public/app/plugins/datasource/graphite/specs/parser_specs.ts
  57. 4 0
      public/app/plugins/datasource/postgres/datasource.ts
  58. 2 2
      public/app/plugins/datasource/postgres/module.ts
  59. 4 2
      public/app/plugins/datasource/prometheus/completer.ts
  60. 1 1
      public/app/plugins/datasource/prometheus/datasource.ts
  61. 3 9
      public/app/plugins/datasource/prometheus/mode-prometheus.js
  62. 11 5
      public/app/plugins/datasource/prometheus/specs/completer_specs.ts
  63. 14 0
      public/app/plugins/datasource/prometheus/specs/datasource_specs.ts
  64. 1 1
      public/app/plugins/panel/gettingstarted/module.ts
  65. 36 1
      public/app/plugins/panel/graph/graph.ts
  66. 2 2
      public/app/plugins/panel/graph/legend.js
  67. 58 9
      public/app/plugins/panel/graph/specs/graph_specs.ts
  68. 11 2
      public/app/plugins/panel/heatmap/color_legend.ts
  69. 3 6
      public/app/plugins/panel/heatmap/heatmap_ctrl.ts
  70. 0 9
      public/app/plugins/panel/heatmap/heatmap_data_converter.ts
  71. 2 3
      public/app/plugins/panel/heatmap/rendering.ts
  72. 19 21
      public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts
  73. 0 2
      public/app/plugins/panel/table/renderer.ts
  74. 18 20
      public/app/plugins/panel/table/specs/renderer.jest.ts
  75. 43 45
      public/app/plugins/panel/table/specs/transformers.jest.ts
  76. 0 2
      public/app/plugins/panel/table/transformers.ts
  77. 0 339
      public/test/core/utils/kbn_specs.js
  78. 4 0
      public/test/jest-setup.ts
  79. 6 0
      public/test/jest-shim.ts
  80. 24 3
      scripts/circle-test.sh
  81. 1 0
      scripts/grunt/default_task.js
  82. 8 1
      scripts/grunt/options/exec.js
  83. 4 0
      scripts/webpack/webpack.prod.js
  84. 2 2
      tslint.json
  85. 505 69
      yarn.lock

+ 2 - 0
.gitignore

@@ -12,6 +12,8 @@ awsconfig
 /tmp
 vendor/phantomjs/phantomjs
 vendor/phantomjs/phantomjs.exe
+profile.out
+coverage.txt
 
 docs/AWS_S3_BUCKET
 docs/GIT_BRANCH

+ 18 - 1
CHANGELOG.md

@@ -7,6 +7,21 @@
 - UX changes to nav & side menu
 - New dashboard grid layout system
 
+# 4.7.0 (unreleased)
+
+## New Features
+* **Data Source Proxy**: Add support for whitelisting specified cookies that will be passed through to the data source when proxying data source requests [#5457](https://github.com/grafana/grafana/issues/5457), thanks [@robingustafsson](https://github.com/robingustafsson)
+
+* **Postgres**: modify group by time macro so it can be used in select clause [#9527](https://github.com/grafana/grafana/pull/9527), thanks [@svenklemm](https://github.com/svenklemm)
+
+## Fixes 
+* **Sensu**: Send alert message to sensu output [#9551](https://github.com/grafana/grafana/issues/9551), thx [@cjchand](https://github.com/cjchand)
+
+# 4.6.0-beta3 (unreleased)
+* **Prometheus**: Fix for browser crash for short time ranges. [#9575](https://github.com/grafana/grafana/issues/9575)
+* **Heatmap**: Fix for y-axis not showing. [#9576](https://github.com/grafana/grafana/issues/9576)
+* **Save to file**: Fix for save to file in export modal. [#9586](https://github.com/grafana/grafana/issues/9586)
+
 # 4.6.0-beta2 (2017-10-17)
 
 ## Fixes
@@ -15,7 +30,9 @@
 * **Cloudwatch**: Provide error message when failing to add cloudwatch datasource [#9534](https://github.com/grafana/grafana/pull/9534), thx [@mtanda](https://github.com/mtanda)
 * **Cloudwatch**: Fix unused period parameter [#9536](https://github.com/grafana/grafana/pull/9536), thx [@mtanda](https://github.com/mtanda)
 * **CSV Export**: Fix for broken CSV export [#9525](https://github.com/grafana/grafana/issues/9525)
-* **Text panel**: Fixes issue with break lines in Firefox [#9491](https://github.com/grafana/grafana/issues/9491)
+* **Text panel**: Fix for issue with break lines in Firefox [#9491](https://github.com/grafana/grafana/issues/9491)
+* **Annotations**: Fix for issue saving annotation event in MySQL DB [#9550](https://github.com/grafana/grafana/issues/9550), thanks [@krise3k](https://github.com/krise3k)
+
 
 # 4.6.0-beta1 (2017-10-13)
 

+ 2 - 1
Gruntfile.js

@@ -22,9 +22,10 @@ module.exports = function (grunt) {
     }
   }
 
+  config.coverage = grunt.option('coverage');
   config.phjs = grunt.option('phjsToRelease');
-
   config.pkg.version = grunt.option('pkgVer') || config.pkg.version;
+
   console.log('Version', config.pkg.version);
 
   // load plugins

+ 17 - 3
README.md

@@ -1,4 +1,4 @@
-[Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana)
+[Grafana](https://grafana.com) [![Circle CI](https://circleci.com/gh/grafana/grafana.svg?style=svg)](https://circleci.com/gh/grafana/grafana) [![Go Report Card](https://goreportcard.com/badge/github.com/grafana/grafana)](https://goreportcard.com/report/github.com/grafana/grafana) [![codecov](https://codecov.io/gh/grafana/grafana/branch/master/graph/badge.svg)](https://codecov.io/gh/grafana/grafana)
 ================
 [Website](https://grafana.com) |
 [Twitter](https://twitter.com/grafana) |
@@ -81,6 +81,20 @@ You only need to add the options you want to override. Config files are applied
 
 In your custom.ini uncomment (remove the leading `;`) sign. And set `app_mode = development`.
 
+### Running tests
+
+- You can run backend Golang tests using "go test ./pkg/...".
+- Execute all frontend tests with "npm run test"
+
+Writing & watching frontend tests (we have two test runners)
+
+- jest for all new tests that do not require browser context (React+more)
+   - Start watcher: `npm run jest`
+   - Jest will run all test files that end with the name ".jest.ts"
+- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
+  - Start watcher: `npm run karma`
+  - Karma+Mocha runs all files that end with the name "_specs.ts".
+
 ## Contribute
 
 If you have any idea for an improvement or found a bug do not hesitate to open an issue.
@@ -89,8 +103,8 @@ the kickass metrics & devops dashboard we all dream about!
 
 ## Plugin development
 
-Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to 
-plugin development. 
+Checkout the [Plugin Development Guide](http://docs.grafana.org/plugins/developing/development/) and checkout the [PLUGIN_DEV.md](https://github.com/grafana/grafana/blob/master/PLUGIN_DEV.md) file for changes in Grafana that relate to
+plugin development.
 
 ## License
 

+ 7 - 7
docs/sources/features/datasources/postgres.md

@@ -48,7 +48,7 @@ Macro example | Description
 *$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > to_timestamp(1494410783) AND dateColumn < to_timestamp(1494497183)*
 *$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)*
 *$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)*
-*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int*
+*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)*
 *$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
 *$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
 *$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
@@ -94,26 +94,26 @@ Example with `metric` column
 
 ```sql
 SELECT
-  min(time_date_time) as time,
+  $__timeGroup(time_date_time,'5m') as time,
   min(value_double),
   'min' as metric
 FROM test_data
 WHERE $__timeFilter(time_date_time)
-GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int
-ORDER BY time asc
+GROUP BY time
+ORDER BY time
 ```
 
 Example with multiple columns:
 
 ```sql
 SELECT
-  min(time_date_time) as time,
+  $__timeGroup(time_date_time,'5m') as time,
   min(value_double) as min_value,
   max(value_double) as max_value
 FROM test_data
 WHERE $__timeFilter(time_date_time)
-GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int
-ORDER BY time asc
+GROUP BY time
+ORDER BY time
 ```
 
 ## Templating

+ 31 - 0
docs/sources/http_api/annotations.md

@@ -120,6 +120,37 @@ Content-Type: application/json
 {"message":"Annotation added"}
 ```
 
+## Create Annotation in Graphite format
+
+Creates an annotation by using Graphite-compatible event format. The `when` and `data` fields are optional. If `when` is not specified then the current time will be used as annotation's timestamp. The `tags` field can also be in prior to Graphite `0.10.0`
+format (string with multiple tags being separated by a space).
+
+`POST /api/annotations/graphite`
+
+**Example Request**:
+
+```json
+POST /api/annotations/graphite HTTP/1.1
+Accept: application/json
+Content-Type: application/json
+
+{
+  "what": "Event - deploy",
+  "tags": ["deploy", "production"],
+  "when": 1467844481,
+  "data": "deploy of master branch happened at Wed Jul 6 22:34:41 UTC 2016"
+}
+```
+
+**Example Response**:
+
+```json
+HTTP/1.1 200
+Content-Type: application/json
+
+{"message":"Graphite annotation added"}
+```
+
 ## Update Annotation
 
 `PUT /api/annotations/:id`

+ 14 - 0
docs/sources/project/building_from_source.md

@@ -84,6 +84,20 @@ bra run
 
 You'll also need to run `npm run watch` to watch for changes to the front-end (typescript, html, sass)
 
+### Running tests
+
+- You can run backend Golang tests using "go test ./pkg/...".
+- Execute all frontend tests with "npm run test"
+
+Writing & watching frontend tests (we have two test runners)
+
+- jest for all new tests that do not require browser context (React+more)
+   - Start watcher: `npm run jest`
+   - Jest will run all test files that end with the name ".jest.ts"
+- karma + mocha is used for testing angularjs components. We do want to migrate these test to jest over time (if possible).
+  - Start watcher: `npm run karma`
+  - Karma+Mocha runs all files that end with the name "_specs.ts".
+
 ## Creating optimized release packages
 
 This step builds linux packages and requires that fpm is installed. Install fpm via `gem install fpm`.

+ 23 - 0
jest.config.js

@@ -0,0 +1,23 @@
+
+module.exports = {
+  verbose: false,
+  "transform": {
+    "^.+\\.tsx?$": "<rootDir>/node_modules/ts-jest/preprocessor.js"
+  },
+  "moduleDirectories": ["<rootDir>node_modules", "<rootDir>/public"],
+  "roots": [
+    "<rootDir>/public"
+  ],
+  "testRegex": "(\\.|/)(jest)\\.(jsx?|tsx?)$",
+  "moduleFileExtensions": [
+    "ts",
+    "tsx",
+    "js",
+    "jsx",
+    "json"
+  ],
+  "setupFiles": [
+    "./public/test/jest-shim.ts",
+    "./public/test/jest-setup.ts"
+  ]
+};

+ 8 - 4
package.json

@@ -12,6 +12,7 @@
   "devDependencies": {
     "@types/d3": "^4.10.1",
     "@types/enzyme": "^2.8.9",
+    "@types/jest": "^21.1.4",
     "@types/node": "^8.0.31",
     "@types/react": "^16.0.5",
     "@types/react-dom": "^15.5.4",
@@ -22,8 +23,8 @@
     "babel-loader": "^7.1.2",
     "babel-preset-es2015": "^6.24.1",
     "css-loader": "^0.28.7",
-    "enzyme": "^3.0.0",
-    "enzyme-adapter-react-16": "^1.0.0",
+    "enzyme": "^3.1.0",
+    "enzyme-adapter-react-16": "^1.0.1",
     "es6-promise": "^3.0.2",
     "es6-shim": "^0.35.3",
     "expect.js": "~0.2.0",
@@ -53,11 +54,11 @@
     "html-loader": "^0.5.1",
     "html-webpack-plugin": "^2.30.1",
     "husky": "^0.14.3",
+    "jest": "^21.2.1",
     "jshint-stylish": "~2.2.1",
     "json-loader": "^0.5.7",
     "karma": "1.7.0",
     "karma-chrome-launcher": "~2.2.0",
-    "karma-coverage": "1.1.1",
     "karma-expect": "~1.1.3",
     "karma-mocha": "~1.3.0",
     "karma-phantomjs-launcher": "1.0.4",
@@ -82,6 +83,7 @@
     "sinon": "1.17.6",
     "systemjs": "0.20.19",
     "systemjs-plugin-css": "^0.1.36",
+    "ts-jest": "^21.1.3",
     "ts-loader": "^2.3.7",
     "tslint": "^5.7.0",
     "tslint-loader": "^3.5.3",
@@ -97,8 +99,10 @@
     "watch": "./node_modules/.bin/webpack --progress --colors --watch --config scripts/webpack/webpack.dev.js",
     "build": "./node_modules/.bin/grunt build",
     "test": "./node_modules/.bin/grunt test",
+    "test-ci": "./node_modules/.bin/grunt test --coverage=true",
     "lint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --type-check",
-    "watch-test": "./node_modules/grunt-cli/bin/grunt karma:dev"
+    "karma": "./node_modules/grunt-cli/bin/grunt karma:dev",
+    "jest": "./node_modules/jest-cli/bin/jest --notify --watch"
   },
   "license": "Apache-2.0",
   "dependencies": {

+ 1 - 1
packaging/deb/init.d/grafana-server

@@ -91,7 +91,7 @@ case "$1" in
 	then
 	  sleep 1
 
-    # check if pid file has been written two
+    # check if pid file has been written to
 	  if ! [[ -s $PID_FILE ]]; then
 	    log_end_msg 1
 	    exit 1

+ 1 - 1
packaging/rpm/init.d/grafana-server

@@ -96,7 +96,7 @@ case "$1" in
     if [ $return -eq 0 ]
     then
       sleep 1
-      # check if pid file has been written two
+      # check if pid file has been written to
       if ! [[ -s $PID_FILE ]]; then
         echo "FAILED"
         exit 1

+ 35 - 14
pkg/api/annotations.go

@@ -1,7 +1,6 @@
 package api
 
 import (
-	"fmt"
 	"strings"
 	"time"
 
@@ -41,9 +40,22 @@ func GetAnnotations(c *middleware.Context) Response {
 	return Json(200, items)
 }
 
+type CreateAnnotationError struct {
+	message string
+}
+
+func (e *CreateAnnotationError) Error() string {
+	return e.message
+}
+
 func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
 	repo := annotations.GetRepository()
 
+	if cmd.Text == "" {
+		err := &CreateAnnotationError{"text field should not be empty"}
+		return ApiError(500, "Failed to save annotation", err)
+	}
+
 	item := annotations.Item{
 		OrgId:       c.OrgId,
 		UserId:      c.UserId,
@@ -55,6 +67,10 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
 		Tags:        cmd.Tags,
 	}
 
+	if item.Epoch == 0 {
+		item.Epoch = time.Now().Unix()
+	}
+
 	if err := repo.Save(&item); err != nil {
 		return ApiError(500, "Failed to save annotation", err)
 	}
@@ -82,21 +98,22 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
 	return ApiSuccess("Annotation added")
 }
 
-type GraphiteAnnotationError struct {
-	message string
-}
-
-func (e *GraphiteAnnotationError) Error() string {
-	return e.message
-}
-
 func formatGraphiteAnnotation(what string, data string) string {
-	return fmt.Sprintf("%s\n%s", what, data)
+	text := what
+	if data != "" {
+		text = text + "\n" + data
+	}
+	return text
 }
 
 func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response {
 	repo := annotations.GetRepository()
 
+	if cmd.What == "" {
+		err := &CreateAnnotationError{"what field should not be empty"}
+		return ApiError(500, "Failed to save Graphite annotation", err)
+	}
+
 	if cmd.When == 0 {
 		cmd.When = time.Now().Unix()
 	}
@@ -106,18 +123,22 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
 	var tagsArray []string
 	switch tags := cmd.Tags.(type) {
 	case string:
-		tagsArray = strings.Split(tags, " ")
+		if tags != "" {
+			tagsArray = strings.Split(tags, " ")
+		} else {
+			tagsArray = []string{}
+		}
 	case []interface{}:
 		for _, t := range tags {
 			if tagStr, ok := t.(string); ok {
 				tagsArray = append(tagsArray, tagStr)
 			} else {
-				err := &GraphiteAnnotationError{"tag should be a string"}
+				err := &CreateAnnotationError{"tag should be a string"}
 				return ApiError(500, "Failed to save Graphite annotation", err)
 			}
 		}
 	default:
-		err := &GraphiteAnnotationError{"unsupported tags format"}
+		err := &CreateAnnotationError{"unsupported tags format"}
 		return ApiError(500, "Failed to save Graphite annotation", err)
 	}
 
@@ -133,7 +154,7 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
 		return ApiError(500, "Failed to save Graphite annotation", err)
 	}
 
-	return ApiSuccess("Graphite Annotation added")
+	return ApiSuccess("Graphite annotation added")
 }
 
 func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {

+ 2 - 0
pkg/models/datasource.go

@@ -18,6 +18,7 @@ const (
 	DS_KAIROSDB      = "kairosdb"
 	DS_PROMETHEUS    = "prometheus"
 	DS_POSTGRES      = "postgres"
+	DS_MYSQL         = "mysql"
 	DS_ACCESS_DIRECT = "direct"
 	DS_ACCESS_PROXY  = "proxy"
 )
@@ -64,6 +65,7 @@ var knownDatasourcePlugins map[string]bool = map[string]bool{
 	DS_PROMETHEUS:                         true,
 	DS_OPENTSDB:                           true,
 	DS_POSTGRES:                           true,
+	DS_MYSQL:                              true,
 	"opennms":                             true,
 	"druid":                               true,
 	"dalmatinerdb":                        true,

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

@@ -112,7 +112,7 @@ func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
 	}
 
 	if evalContext.Rule.Message != "" {
-		bodyJSON.Set("message", evalContext.Rule.Message)
+		bodyJSON.Set("output", evalContext.Rule.Message)
 	}
 
 	body, _ := bodyJSON.MarshalJSON()

+ 2 - 2
pkg/tsdb/postgres/macros.go

@@ -74,7 +74,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)
 		}
-		return fmt.Sprintf("%s >= to_timestamp(%d) AND %s <= to_timestamp(%d)", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
+		return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
 	case "__timeFrom":
 		return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
 	case "__timeTo":
@@ -83,7 +83,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
 		if len(args) < 2 {
 			return "", fmt.Errorf("macro %v needs time column and interval", name)
 		}
-		return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int", args[0], args[1]), nil
+		return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int*extract(epoch from %s::interval)", args[0], args[1], args[1]), nil
 	case "__unixEpochFilter":
 		if len(args) == 0 {
 			return "", fmt.Errorf("missing time column argument for macro %v", name)

+ 2 - 2
pkg/tsdb/postgres/macros_test.go

@@ -30,7 +30,7 @@ func TestMacroEngine(t *testing.T) {
 			sql, err := engine.Interpolate(timeRange, "WHERE $__timeFilter(time_column)")
 			So(err, ShouldBeNil)
 
-			So(sql, ShouldEqual, "WHERE time_column >= to_timestamp(18446744066914186738) AND time_column <= to_timestamp(18446744066914187038)")
+			So(sql, ShouldEqual, "WHERE extract(epoch from time_column) BETWEEN 18446744066914186738 AND 18446744066914187038")
 		})
 
 		Convey("interpolate __timeFrom function", func() {
@@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) {
 			sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
 			So(err, ShouldBeNil)
 
-			So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int")
+			So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)")
 		})
 
 		Convey("interpolate __timeTo function", func() {

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

@@ -0,0 +1,8 @@
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import { PasswordStrength } from './ui/PasswordStrength';
+
+export function registerAngularDirectives() {
+
+  react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
+
+}

+ 4 - 3
public/app/core/core.ts

@@ -43,7 +43,6 @@ import {assignModelProperties} from './utils/model_utils';
 import {contextSrv} from './services/context_srv';
 import {KeybindingSrv} from './services/keybindingSrv';
 import {helpModal} from './components/help/help';
-import {PasswordStrength} from './components/PasswordStrength';
 import {JsonExplorer} from './components/json_explorer/json_explorer';
 import {NavModelSrv, NavModel} from './nav_model_srv';
 import {userPicker} from './components/user_picker';
@@ -52,6 +51,9 @@ import {geminiScrollbar} from './components/scroll/scroll';
 import {gfPageDirective} from './components/gf_page';
 import {orgSwitcher} from './components/org_switcher';
 import {profiler} from './profiler';
+import {registerAngularDirectives} from './angular_wrappers';
+
+registerAngularDirectives();
 
 export {
   profiler,
@@ -82,6 +84,5 @@ export {
   userGroupPicker,
   geminiScrollbar,
   gfPageDirective,
-  orgSwitcher,
-  PasswordStrength,
+  orgSwitcher
 };

+ 2 - 0
public/app/core/directives/misc.js

@@ -7,6 +7,8 @@ define([
 function (angular, require, coreModule, kbn) {
   'use strict';
 
+  kbn = kbn.default;
+
   coreModule.default.directive('tip', function($compile) {
     return {
       restrict: 'E',

+ 1 - 2
public/app/core/directives/ng_model_on_blur.js

@@ -1,9 +1,8 @@
 define([
   '../core_module',
-  'app/core/utils/kbn',
   'app/core/utils/rangeutil',
 ],
-function (coreModule, kbn, rangeUtil) {
+function (coreModule, rangeUtil) {
   'use strict';
 
   coreModule.default.directive('ngModelOnblur', function() {

+ 4 - 5
public/app/core/specs/PasswordStrength_specs.tsx → public/app/core/specs/PasswordStrength.jest.tsx

@@ -1,24 +1,23 @@
 import React from 'react';
-import {describe, it, expect} from 'test/lib/common';
 import {shallow} from 'enzyme';
 
-import {PasswordStrength} from '../components/PasswordStrength';
+import {PasswordStrength} from '../ui/PasswordStrength';
 
 describe('PasswordStrength', () => {
 
   it('should have class bad if length below 4', () => {
     const wrapper = shallow(<PasswordStrength password="asd" />);
-    expect(wrapper.find(".password-strength-bad")).to.have.length(1);
+    expect(wrapper.find(".password-strength-bad")).toHaveLength(1);
   });
 
   it('should have class ok if length below 8', () => {
     const wrapper = shallow(<PasswordStrength password="asdasd" />);
-    expect(wrapper.find(".password-strength-ok")).to.have.length(1);
+    expect(wrapper.find(".password-strength-ok")).toHaveLength(1);
   });
 
   it('should have class good if length above 8', () => {
     const wrapper = shallow(<PasswordStrength password="asdaasdda" />);
-    expect(wrapper.find(".password-strength-good")).to.have.length(1);
+    expect(wrapper.find(".password-strength-good")).toHaveLength(1);
   });
 
 });

+ 19 - 19
public/test/core/utils/datemath_specs.ts → public/app/core/specs/datemath.jest.ts

@@ -1,4 +1,4 @@
-import {describe, beforeEach, afterEach, it, sinon, expect} from 'test/lib/common';
+import sinon from 'sinon';
 
 import * as dateMath from 'app/core/utils/datemath';
 import moment from 'moment';
@@ -13,25 +13,25 @@ describe("DateMath", () => {
 
   describe('errors', () => {
     it('should return undefined if passed something falsy', () => {
-      expect(dateMath.parse(false)).to.be(undefined);
+      expect(dateMath.parse(false)).toBe(undefined);
     });
 
     it('should return undefined if I pass an operator besides [+-/]', () => {
-      expect(dateMath.parse('now&1d')).to.be(undefined);
+      expect(dateMath.parse('now&1d')).toBe(undefined);
     });
 
     it('should return undefined if I pass a unit besides' + spans.toString(), () => {
-      expect(dateMath.parse('now+5f')).to.be(undefined);
+      expect(dateMath.parse('now+5f')).toBe(undefined);
     });
 
     it('should return undefined if rounding unit is not 1', () => {
-      expect(dateMath.parse('now/2y')).to.be(undefined);
-      expect(dateMath.parse('now/0.5y')).to.be(undefined);
+      expect(dateMath.parse('now/2y')).toBe(undefined);
+      expect(dateMath.parse('now/0.5y')).toBe(undefined);
     });
 
     it('should not go into an infinite loop when missing a unit', () => {
-      expect(dateMath.parse('now-0')).to.be(undefined);
-      expect(dateMath.parse('now-00')).to.be(undefined);
+      expect(dateMath.parse('now-0')).toBe(undefined);
+      expect(dateMath.parse('now-00')).toBe(undefined);
     });
   });
 
@@ -43,7 +43,7 @@ describe("DateMath", () => {
     expected.setMilliseconds(0);
 
     var startOfDay = dateMath.parse('now/d', false).valueOf();
-    expect(startOfDay).to.be(expected.getTime());
+    expect(startOfDay).toBe(expected.getTime());
   });
 
   it("now/d on a utc dashboard should be start of the current day in UTC time", () => {
@@ -51,7 +51,7 @@ describe("DateMath", () => {
     var expected = new Date(Date.UTC(today.getFullYear(), today.getMonth(), today.getDate(), 0, 0, 0, 0));
 
     var startOfDay = dateMath.parse('now/d', false, 'utc').valueOf();
-    expect(startOfDay).to.be(expected.getTime());
+    expect(startOfDay).toBe(expected.getTime());
   });
 
   describe('subtraction', () => {
@@ -69,11 +69,11 @@ describe("DateMath", () => {
       var thenEx =  anchor + '||-5' + span;
 
       it('should return 5' + span + ' ago', () => {
-        expect(dateMath.parse(nowEx).format(format)).to.eql(now.subtract(5, span).format(format));
+        expect(dateMath.parse(nowEx).format(format)).toEqual(now.subtract(5, span).format(format));
       });
 
       it('should return 5' + span + ' before ' + anchor, () => {
-        expect(dateMath.parse(thenEx).format(format)).to.eql(anchored.subtract(5, span).format(format));
+        expect(dateMath.parse(thenEx).format(format)).toEqual(anchored.subtract(5, span).format(format));
       });
     });
 
@@ -94,11 +94,11 @@ describe("DateMath", () => {
 
     _.each(spans, (span) => {
       it('should round now to the beginning of the ' + span, function () {
-        expect(dateMath.parse('now/' + span).format(format)).to.eql(now.startOf(span).format(format));
+        expect(dateMath.parse('now/' + span).format(format)).toEqual(now.startOf(span).format(format));
       });
 
       it('should round now to the end of the ' + span, function () {
-        expect(dateMath.parse('now/' + span, true).format(format)).to.eql(now.endOf(span).format(format));
+        expect(dateMath.parse('now/' + span, true).format(format)).toEqual(now.endOf(span).format(format));
       });
     });
 
@@ -109,27 +109,27 @@ describe("DateMath", () => {
 
   describe('isValid', () => {
     it('should return false when invalid date text', () => {
-      expect(dateMath.isValid('asd')).to.be(false);
+      expect(dateMath.isValid('asd')).toBe(false);
     });
     it('should return true when valid date text', () => {
-      expect(dateMath.isValid('now-1h')).to.be(true);
+      expect(dateMath.isValid('now-1h')).toBe(true);
     });
   });
 
   describe('relative time to date parsing', function() {
     it('should handle negative time', function() {
       var date = dateMath.parseDateMath('-2d', moment([2014, 1, 5]));
-      expect(date.valueOf()).to.equal(moment([2014, 1, 3]).valueOf());
+      expect(date.valueOf()).toEqual(moment([2014, 1, 3]).valueOf());
     });
 
     it('should handle multiple math expressions', function() {
       var date = dateMath.parseDateMath('-2d-6h', moment([2014, 1, 5]));
-      expect(date.valueOf()).to.equal(moment([2014, 1, 2, 18]).valueOf());
+      expect(date.valueOf()).toEqual(moment([2014, 1, 2, 18]).valueOf());
     });
 
     it('should return false when invalid expression', function() {
       var date = dateMath.parseDateMath('2', moment([2014, 1, 5]));
-      expect(date).to.equal(undefined);
+      expect(date).toEqual(undefined);
     });
   });
 

+ 6 - 8
public/test/core/utils/emitter_specs.ts → public/app/core/specs/emitter.jest.ts

@@ -1,6 +1,4 @@
-import {describe, it, expect} from 'test/lib/common';
-
-import {Emitter} from 'app/core/core';
+import {Emitter} from '../utils/emitter';
 
 describe("Emitter", () => {
 
@@ -20,8 +18,8 @@ describe("Emitter", () => {
 
       events.emit('test', null);
 
-      expect(sub1Called).to.be(true);
-      expect(sub2Called).to.be(true);
+      expect(sub1Called).toBe(true);
+      expect(sub2Called).toBe(true);
     });
 
     it('when subscribing twice', () => {
@@ -37,7 +35,7 @@ describe("Emitter", () => {
 
       events.emit('test', null);
 
-      expect(sub1Called).to.be(2);
+      expect(sub1Called).toBe(2);
     });
 
     it('should handle errors', () => {
@@ -57,8 +55,8 @@ describe("Emitter", () => {
       try { events.emit('test', null); } catch (_) { }
       try { events.emit('test', null); } catch (_) {}
 
-      expect(sub1Called).to.be(2);
-      expect(sub2Called).to.be(0);
+      expect(sub1Called).toBe(2);
+      expect(sub2Called).toBe(0);
     });
   });
 });

+ 3 - 5
public/test/core/utils/flatten_specs.ts → public/app/core/specs/flatten.jest.ts

@@ -1,5 +1,3 @@
-import {describe, it, expect} from 'test/lib/common';
-
 import flatten from 'app/core/utils/flatten';
 
 describe("flatten", () => {
@@ -15,9 +13,9 @@ describe("flatten", () => {
       }
     }, null);
 
-    expect(flattened['level1']).to.be('level1-value');
-    expect(flattened['deeper.level2']).to.be('level2-value');
-    expect(flattened['deeper.deeper.level3']).to.be('level3-value');
+    expect(flattened['level1']).toBe('level1-value');
+    expect(flattened['deeper.level2']).toBe('level2-value');
+    expect(flattened['deeper.deeper.level3']).toBe('level3-value');
   });
 
 });

+ 347 - 0
public/app/core/specs/kbn.jest.ts

@@ -0,0 +1,347 @@
+import kbn from '../utils/kbn';
+import * as dateMath from '../utils/datemath';
+import moment from 'moment';
+
+describe('unit format menu', function() {
+  var menu = kbn.getUnitFormats();
+  menu.map(function(submenu) {
+
+    describe('submenu ' + submenu.text, function() {
+
+      it('should have a title', function() {
+        expect(typeof submenu.text).toBe('string');
+      });
+
+      it('should have a submenu', function() {
+        expect(Array.isArray(submenu.submenu)).toBe(true);
+      });
+
+      submenu.submenu.map(function(entry) {
+        describe('entry ' + entry.text, function() {
+          it('should have a title', function() { expect(typeof entry.text).toBe('string'); });
+          it('should have a format', function() { expect(typeof entry.value).toBe('string'); });
+          it('should have a valid format', function() {
+            expect(typeof kbn.valueFormats[entry.value]).toBe('function');
+          });
+        });
+      });
+    });
+  });
+});
+
+function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
+
+  describe('value format: ' + desc, function() {
+    it('should translate ' + value + ' as ' + result, function() {
+      var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
+      var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
+      expect(str).toBe(result);
+    });
+  });
+
+}
+
+describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
+describeValueFormat('ms', 100, 1, 0, '100 ms');
+describeValueFormat('ms', 1250, 10, 0, '1.25 s');
+describeValueFormat('ms', 1250, 300, 0, '1.3 s');
+describeValueFormat('ms', 65150, 10000, 0, '1.1 min');
+describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour');
+describeValueFormat('ms', 651500000, 150000000, 0, '8 day');
+
+describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
+describeValueFormat('none', 0, 0, 2, '0');
+describeValueFormat('dB', 10, 1000, 2, '10.00 dB');
+
+describeValueFormat('percent',  0, 0, 0, '0%');
+describeValueFormat('percent', 53, 0, 1, '53.0%');
+describeValueFormat('percentunit', 0.0, 0, 0, '0%');
+describeValueFormat('percentunit', 0.278, 0, 1, '27.8%');
+describeValueFormat('percentunit', 1.0, 0, 0, '100%');
+
+describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42');
+describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K');
+describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M');
+
+describeValueFormat('bytes', -1.57e+308, -1.57e+308, 2, 'NA');
+
+describeValueFormat('ns', 25, 1, 0, '25 ns');
+describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
+
+describeValueFormat('ops', 123, 1, 0, '123 ops');
+describeValueFormat('rps', 456000, 1000, -1, '456K rps');
+describeValueFormat('rps', 123456789, 1000000, 2, '123.457M rps');
+describeValueFormat('wps', 789000000, 1000000, -1, '789M wps');
+describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops');
+
+describeValueFormat('s', 1.23456789e-7, 1e-10, 8, '123.5 ns');
+describeValueFormat('s', 1.23456789e-4, 1e-7, 5, '123.5 µs');
+describeValueFormat('s', 1.23456789e-3, 1e-6, 4, '1.235 ms');
+describeValueFormat('s', 1.23456789e-2, 1e-5, 3, '12.35 ms');
+describeValueFormat('s', 1.23456789e-1, 1e-4, 2, '123.5 ms');
+describeValueFormat('s', 24, 1, 0, '24 s');
+describeValueFormat('s', 246, 1, 0, '4.1 min');
+describeValueFormat('s', 24567, 100, 0, '6.82 hour');
+describeValueFormat('s', 24567890, 10000, 0, '40.62 week');
+describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year');
+
+describeValueFormat('m', 24, 1, 0, '24 min');
+describeValueFormat('m', 246, 10, 0, '4.1 hour');
+describeValueFormat('m', 6545, 10, 0, '4.55 day');
+describeValueFormat('m', 24567, 100, 0, '2.44 week');
+describeValueFormat('m', 24567892, 10000, 0, '46.7 year');
+
+describeValueFormat('h', 21, 1, 0, '21 hour');
+describeValueFormat('h', 145, 1, 0, '6.04 day');
+describeValueFormat('h', 1234, 100, 0, '7.3 week');
+describeValueFormat('h', 9458, 1000, 0, '1.08 year');
+
+describeValueFormat('d', 3, 1, 0, '3 day');
+describeValueFormat('d', 245, 100, 0, '35 week');
+describeValueFormat('d', 2456, 10, 0, '6.73 year');
+
+describe('date time formats', function() {
+  it('should format as iso date', function() {
+    var str = kbn.valueFormats.dateTimeAsIso(1505634997920, 1);
+    expect(str).toBe(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
+  });
+
+  it('should format as iso date and skip date when today', function() {
+    var now = moment();
+    var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1);
+    expect(str).toBe(now.format("HH:mm:ss"));
+  });
+
+  it('should format as US date', function() {
+    var str = kbn.valueFormats.dateTimeAsUS(1505634997920, 1);
+    expect(str).toBe(moment(1505634997920).format('MM/DD/YYYY H:mm:ss a'));
+  });
+
+  it('should format as US date and skip date when today', function() {
+    var now = moment();
+    var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1);
+    expect(str).toBe(now.format("h:mm:ss a"));
+  });
+
+  it('should format as from now with days', function() {
+    var daysAgo = moment().add(-7, 'd');
+    var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1);
+    expect(str).toBe('7 days ago');
+  });
+
+  it('should format as from now with minutes', function() {
+    var daysAgo = moment().add(-2, 'm');
+    var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1);
+    expect(str).toBe('2 minutes ago');
+  });
+});
+
+describe('kbn.toFixed and negative decimals', function() {
+  it('should treat as zero decimals', function() {
+    var str = kbn.toFixed(186.123, -2);
+    expect(str).toBe('186');
+  });
+});
+
+describe('kbn ms format when scaled decimals is null do not use it', function() {
+  it('should use specified decimals', function() {
+    var str = kbn.valueFormats['ms'](10000086.123, 1, null);
+    expect(str).toBe('2.8 hour');
+  });
+});
+
+describe('kbn kbytes format when scaled decimals is null do not use it', function() {
+  it('should use specified decimals', function() {
+    var str = kbn.valueFormats['kbytes'](10000000, 3, null);
+    expect(str).toBe('9.537 GiB');
+  });
+});
+
+describe('kbn deckbytes format when scaled decimals is null do not use it', function() {
+  it('should use specified decimals', function() {
+    var str = kbn.valueFormats['deckbytes'](10000000, 3, null);
+    expect(str).toBe('10.000 GB');
+  });
+});
+
+describe('kbn roundValue', function() {
+  it('should should handle null value', function() {
+    var str = kbn.roundValue(null, 2);
+    expect(str).toBe(null);
+  });
+  it('should round value', function() {
+    var str = kbn.roundValue(200.877, 2);
+    expect(str).toBe(200.88);
+  });
+});
+
+describe('calculateInterval', function() {
+  it('1h 100 resultion', function() {
+    var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 100, null);
+    expect(res.interval).toBe('30s');
+  });
+
+  it('10m 1600 resolution', function() {
+    var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 1600, null);
+    expect(res.interval).toBe('500ms');
+    expect(res.intervalMs).toBe(500);
+  });
+
+  it('fixed user min interval', function() {
+    var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')};
+    var res = kbn.calculateInterval(range, 1600, '10s');
+    expect(res.interval).toBe('10s');
+    expect(res.intervalMs).toBe(10000);
+  });
+
+  it('short time range and user low limit', function() {
+    var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 1600, '>10s');
+    expect(res.interval).toBe('10s');
+  });
+
+  it('large time range and user low limit', function() {
+    var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
+    var res = kbn.calculateInterval(range, 1000, '>10s');
+    expect(res.interval).toBe('20m');
+  });
+
+  it('10s 900 resolution and user low limit in ms', function() {
+    var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 900, '>15ms');
+    expect(res.interval).toBe('15ms');
+  });
+
+  it('1d 1 resolution', function() {
+    var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 1, null);
+    expect(res.interval).toBe('1d');
+    expect(res.intervalMs).toBe(86400000);
+  });
+
+  it('86399s 1 resolution', function() {
+    var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') };
+    var res = kbn.calculateInterval(range, 1, null);
+    expect(res.interval).toBe('12h');
+    expect(res.intervalMs).toBe(43200000);
+  });
+});
+
+describe('hex', function() {
+  it('positive integer', function() {
+    var str = kbn.valueFormats.hex(100, 0);
+    expect(str).toBe('64');
+  });
+  it('negative integer', function() {
+    var str = kbn.valueFormats.hex(-100, 0);
+    expect(str).toBe('-64');
+  });
+  it('null', function() {
+    var str = kbn.valueFormats.hex(null, 0);
+    expect(str).toBe('');
+  });
+  it('positive float', function() {
+    var str = kbn.valueFormats.hex(50.52, 1);
+    expect(str).toBe('32.8');
+  });
+  it('negative float', function() {
+    var str = kbn.valueFormats.hex(-50.333, 2);
+    expect(str).toBe('-32.547AE147AE14');
+  });
+});
+
+describe('hex 0x', function() {
+  it('positive integeter', function() {
+    var str = kbn.valueFormats.hex0x(7999,0);
+    expect(str).toBe('0x1F3F');
+  });
+  it('negative integer', function() {
+    var str = kbn.valueFormats.hex0x(-584,0);
+    expect(str).toBe('-0x248');
+  });
+  it('null', function() {
+    var str = kbn.valueFormats.hex0x(null, 0);
+    expect(str).toBe('');
+  });
+  it('positive float', function() {
+    var str = kbn.valueFormats.hex0x(74.443, 3);
+    expect(str).toBe('0x4A.716872B020C4');
+  });
+  it('negative float', function() {
+    var str = kbn.valueFormats.hex0x(-65.458, 1);
+    expect(str).toBe('-0x41.8');
+  });
+});
+
+describe('duration', function() {
+  it('null', function() {
+    var str = kbn.toDuration(null, 0, "millisecond");
+    expect(str).toBe('');
+  });
+  it('0 milliseconds', function() {
+    var str = kbn.toDuration(0, 0, "millisecond");
+    expect(str).toBe('0 milliseconds');
+  });
+  it('1 millisecond', function() {
+    var str = kbn.toDuration(1, 0, "millisecond");
+    expect(str).toBe('1 millisecond');
+  });
+  it('-1 millisecond', function() {
+    var str = kbn.toDuration(-1, 0, "millisecond");
+    expect(str).toBe('1 millisecond ago');
+  });
+  it('seconds', function() {
+    var str = kbn.toDuration(1, 0, "second");
+    expect(str).toBe('1 second');
+  });
+  it('minutes', function() {
+    var str = kbn.toDuration(1, 0, "minute");
+    expect(str).toBe('1 minute');
+  });
+  it('hours', function() {
+    var str = kbn.toDuration(1, 0, "hour");
+    expect(str).toBe('1 hour');
+  });
+  it('days', function() {
+    var str = kbn.toDuration(1, 0, "day");
+    expect(str).toBe('1 day');
+  });
+  it('weeks', function() {
+    var str = kbn.toDuration(1, 0, "week");
+    expect(str).toBe('1 week');
+  });
+  it('months', function() {
+    var str = kbn.toDuration(1, 0, "month");
+    expect(str).toBe('1 month');
+  });
+  it('years', function() {
+    var str = kbn.toDuration(1, 0, "year");
+    expect(str).toBe('1 year');
+  });
+  it('decimal days', function() {
+    var str = kbn.toDuration(1.5, 2, "day");
+    expect(str).toBe('1 day, 12 hours, 0 minutes');
+  });
+  it('decimal months', function() {
+    var str = kbn.toDuration(1.5, 3, "month");
+    expect(str).toBe('1 month, 2 weeks, 1 day, 0 hours');
+  });
+  it('no decimals', function() {
+    var str = kbn.toDuration(38898367008, 0, "millisecond");
+    expect(str).toBe('1 year');
+  });
+  it('1 decimal', function() {
+    var str = kbn.toDuration(38898367008, 1, "millisecond");
+    expect(str).toBe('1 year, 2 months');
+  });
+  it('too many decimals', function() {
+    var str = kbn.toDuration(38898367008, 20, "millisecond");
+    expect(str).toBe('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
+  });
+  it('floating point error', function() {
+    var str = kbn.toDuration(36993906007, 8, "millisecond");
+    expect(str).toBe('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
+  });
+});

+ 22 - 24
public/test/core/utils/rangeutil_specs.ts → public/app/core/specs/rangeutil.jest.ts

@@ -1,5 +1,3 @@
-import {describe, it, expect} from 'test/lib/common';
-
 import * as rangeUtil from 'app/core/utils/rangeutil';
 import _ from 'lodash';
 import moment from 'moment';
@@ -9,100 +7,100 @@ describe("rangeUtil", () => {
   describe("Can get range grouped list of ranges", () => {
     it('when custom settings should return default range list', () => {
       var groups = rangeUtil.getRelativeTimesList({time_options: []}, 'Last 5 minutes');
-      expect(_.keys(groups).length).to.be(4);
-      expect(groups[3][0].active).to.be(true);
+      expect(_.keys(groups).length).toBe(4);
+      expect(groups[3][0].active).toBe(true);
     });
   });
 
   describe("Can get range text described", () => {
     it('should handle simple old expression with only amount and unit', () => {
       var info = rangeUtil.describeTextRange('5m');
-      expect(info.display).to.be('Last 5 minutes');
+      expect(info.display).toBe('Last 5 minutes');
     });
 
     it('should have singular when amount is 1', () => {
       var info = rangeUtil.describeTextRange('1h');
-      expect(info.display).to.be('Last 1 hour');
+      expect(info.display).toBe('Last 1 hour');
     });
 
     it('should handle non default amount', () => {
       var info = rangeUtil.describeTextRange('13h');
-      expect(info.display).to.be('Last 13 hours');
-      expect(info.from).to.be('now-13h');
+      expect(info.display).toBe('Last 13 hours');
+      expect(info.from).toBe('now-13h');
     });
 
     it('should handle non default future amount', () => {
       var info = rangeUtil.describeTextRange('+3h');
-      expect(info.display).to.be('Next 3 hours');
-      expect(info.from).to.be('now');
-      expect(info.to).to.be('now+3h');
+      expect(info.display).toBe('Next 3 hours');
+      expect(info.from).toBe('now');
+      expect(info.to).toBe('now+3h');
     });
 
     it('should handle now/d', () => {
       var info = rangeUtil.describeTextRange('now/d');
-      expect(info.display).to.be('Today so far');
+      expect(info.display).toBe('Today so far');
     });
 
     it('should handle now/w', () => {
       var info = rangeUtil.describeTextRange('now/w');
-      expect(info.display).to.be('This week so far');
+      expect(info.display).toBe('This week so far');
     });
 
     it('should handle now/M', () => {
       var info = rangeUtil.describeTextRange('now/M');
-      expect(info.display).to.be('This month so far');
+      expect(info.display).toBe('This month so far');
     });
 
     it('should handle now/y', () => {
       var info = rangeUtil.describeTextRange('now/y');
-      expect(info.display).to.be('This year so far');
+      expect(info.display).toBe('This year so far');
     });
   });
 
   describe("Can get date range described", () => {
     it('Date range with simple ranges', () => {
       var text = rangeUtil.describeTimeRange({from: 'now-1h', to: 'now'});
-      expect(text).to.be('Last 1 hour');
+      expect(text).toBe('Last 1 hour');
     });
 
     it('Date range with rounding ranges', () => {
       var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now'});
-      expect(text).to.be('now/d+6h to now');
+      expect(text).toBe('now/d+6h to now');
     });
 
     it('Date range with absolute to now', () => {
       var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now'});
-      expect(text).to.be('Nov 10, 2014 02:03:04 to a few seconds ago');
+      expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago');
     });
 
     it('Date range with absolute to relative', () => {
       var text = rangeUtil.describeTimeRange({from: moment([2014,10,10,2,3,4]), to: 'now-1d'});
-      expect(text).to.be('Nov 10, 2014 02:03:04 to a day ago');
+      expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago');
     });
 
     it('Date range with relative to absolute', () => {
       var text = rangeUtil.describeTimeRange({from: 'now-7d', to: moment([2014,10,10,2,3,4])});
-      expect(text).to.be('7 days ago to Nov 10, 2014 02:03:04');
+      expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04');
     });
 
     it('Date range with non matching default ranges', () => {
       var text = rangeUtil.describeTimeRange({from: 'now-13h', to: 'now'});
-      expect(text).to.be('Last 13 hours');
+      expect(text).toBe('Last 13 hours');
     });
 
     it('Date range with from and to both are in now-* format', () => {
       var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now-3h'});
-      expect(text).to.be('now-6h to now-3h');
+      expect(text).toBe('now-6h to now-3h');
     });
 
     it('Date range with from and to both are either in now-* or now/* format', () => {
       var text = rangeUtil.describeTimeRange({from: 'now/d+6h', to: 'now-3h'});
-      expect(text).to.be('now/d+6h to now-3h');
+      expect(text).toBe('now/d+6h to now-3h');
     });
 
     it('Date range with from and to both are either in now-* or now+* format', () => {
       var text = rangeUtil.describeTimeRange({from: 'now-6h', to: 'now+1h'});
-      expect(text).to.be('now-6h to now+1h');
+      expect(text).toBe('now-6h to now+1h');
     });
 
   });

+ 8 - 10
public/test/core/table_model_specs.ts → public/app/core/specs/table_model.jest.ts

@@ -1,5 +1,3 @@
-import {describe, beforeEach, it, expect} from 'test/lib/common';
-
 import TableModel from 'app/core/table_model';
 
 describe('when sorting table desc', () => {
@@ -16,14 +14,14 @@ describe('when sorting table desc', () => {
   });
 
   it('should sort by time', () => {
-    expect(table.rows[0][0]).to.be(105);
-    expect(table.rows[1][0]).to.be(103);
-    expect(table.rows[2][0]).to.be(100);
+    expect(table.rows[0][0]).toBe(105);
+    expect(table.rows[1][0]).toBe(103);
+    expect(table.rows[2][0]).toBe(100);
   });
 
   it ('should mark column being sorted', () => {
-    expect(table.columns[0].sort).to.be(true);
-    expect(table.columns[0].desc).to.be(true);
+    expect(table.columns[0].sort).toBe(true);
+    expect(table.columns[0].desc).toBe(true);
   });
 
 });
@@ -42,9 +40,9 @@ describe('when sorting table asc', () => {
   });
 
   it('should sort by time', () => {
-    expect(table.rows[0][1]).to.be(10);
-    expect(table.rows[1][1]).to.be(11);
-    expect(table.rows[2][1]).to.be(15);
+    expect(table.rows[0][1]).toBe(10);
+    expect(table.rows[1][1]).toBe(11);
+    expect(table.rows[2][1]).toBe(15);
   });
 
 });

+ 41 - 42
public/app/core/specs/time_series_specs.ts → public/app/core/specs/time_series.jest.ts

@@ -1,4 +1,3 @@
-import {describe, beforeEach, it, expect} from 'test/lib/common';
 import TimeSeries from 'app/core/time_series2';
 
 describe("TimeSeries", function() {
@@ -19,14 +18,14 @@ describe("TimeSeries", function() {
     it('with connected style, should ignore nulls', function() {
       series = new TimeSeries(testData);
       points = series.getFlotPairs('connected', yAxisFormats);
-      expect(points.length).to.be(3);
+      expect(points.length).toBe(3);
     });
 
     it('with null as zero style, should replace nulls with zero', function() {
       series = new TimeSeries(testData);
       points = series.getFlotPairs('null as zero', yAxisFormats);
-      expect(points.length).to.be(4);
-      expect(points[1][1]).to.be(0);
+      expect(points.length).toBe(4);
+      expect(points[1][1]).toBe(0);
     });
 
     it('if last is null current should pick next to last', function() {
@@ -34,7 +33,7 @@ describe("TimeSeries", function() {
         datapoints: [[10,1], [null, 2]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.current).to.be(10);
+      expect(series.stats.current).toBe(10);
     });
 
     it('max value should work for negative values', function() {
@@ -42,13 +41,13 @@ describe("TimeSeries", function() {
         datapoints: [[-10,1], [-4, 2]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.max).to.be(-4);
+      expect(series.stats.max).toBe(-4);
     });
 
     it('average value should ignore nulls', function() {
       series = new TimeSeries(testData);
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.avg).to.be(6.333333333333333);
+      expect(series.stats.avg).toBe(6.333333333333333);
     });
 
     it('the delta value should account for nulls', function() {
@@ -56,7 +55,7 @@ describe("TimeSeries", function() {
         datapoints: [[1,2],[3,3],[null,4],[10,5],[15,6]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.delta).to.be(14);
+      expect(series.stats.delta).toBe(14);
     });
 
     it('the delta value should account for nulls on first', function() {
@@ -64,7 +63,7 @@ describe("TimeSeries", function() {
         datapoints: [[null,2],[1,3],[10,4],[15,5]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.delta).to.be(14);
+      expect(series.stats.delta).toBe(14);
     });
 
     it('the delta value should account for nulls on last', function() {
@@ -72,7 +71,7 @@ describe("TimeSeries", function() {
         datapoints: [[1,2],[5,3],[10,4],[null,5]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.delta).to.be(9);
+      expect(series.stats.delta).toBe(9);
     });
 
     it('the delta value should account for resets', function() {
@@ -80,7 +79,7 @@ describe("TimeSeries", function() {
         datapoints: [[1,2],[5,3],[10,4],[0,5],[10,6]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.delta).to.be(19);
+      expect(series.stats.delta).toBe(19);
     });
 
     it('the delta value should account for resets on last', function() {
@@ -88,30 +87,30 @@ describe("TimeSeries", function() {
         datapoints: [[1,2],[2,3],[10,4],[8,5]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.delta).to.be(17);
+      expect(series.stats.delta).toBe(17);
     });
 
     it('the range value should be max - min', function() {
       series = new TimeSeries(testData);
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.range).to.be(9);
+      expect(series.stats.range).toBe(9);
     });
 
     it('first value should ingone nulls', function() {
       series = new TimeSeries(testData);
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.first).to.be(1);
+      expect(series.stats.first).toBe(1);
       series = new TimeSeries({
         datapoints: [[null,2],[1,3],[10,4],[8,5]]
       });
       series.getFlotPairs('null', yAxisFormats);
-      expect(series.stats.first).to.be(1);
+      expect(series.stats.first).toBe(1);
     });
 
     it('with null as zero style, average value should treat nulls as 0', function() {
       series = new TimeSeries(testData);
       series.getFlotPairs('null as zero', yAxisFormats);
-      expect(series.stats.avg).to.be(4.75);
+      expect(series.stats.avg).toBe(4.75);
     });
 
     it('average value should be null if all values is null', function() {
@@ -119,7 +118,7 @@ describe("TimeSeries", function() {
         datapoints: [[null,2],[null,3],[null,4],[null,5]]
       });
       series.getFlotPairs('null');
-      expect(series.stats.avg).to.be(null);
+      expect(series.stats.avg).toBe(null);
     });
   });
 
@@ -130,7 +129,7 @@ describe("TimeSeries", function() {
       });
 
       it('should set hasMsResolution to false', function() {
-        expect(series.hasMsResolution).to.be(false);
+        expect(series.hasMsResolution).toBe(false);
       });
     });
 
@@ -140,7 +139,7 @@ describe("TimeSeries", function() {
       });
 
       it('should show millisecond resolution tooltip', function() {
-        expect(series.hasMsResolution).to.be(true);
+        expect(series.hasMsResolution).toBe(true);
       });
     });
 
@@ -150,7 +149,7 @@ describe("TimeSeries", function() {
       });
 
       it('should not show millisecond resolution tooltip', function() {
-        expect(series.hasMsResolution).to.be(false);
+        expect(series.hasMsResolution).toBe(false);
       });
     });
   });
@@ -165,13 +164,13 @@ describe("TimeSeries", function() {
     it('missing datapoint with ms precision', function() {
       fakedata.datapoints[0] = [1337, 1234567890000];
       series = new TimeSeries(fakedata);
-      expect(series.isMsResolutionNeeded()).to.be(false);
+      expect(series.isMsResolutionNeeded()).toBe(false);
     });
 
     it('contains datapoint with ms precision', function() {
       fakedata.datapoints[0] = [1337, 1236547890001];
       series = new TimeSeries(fakedata);
-      expect(series.isMsResolutionNeeded()).to.be(true);
+      expect(series.isMsResolutionNeeded()).toBe(true);
     });
   });
 
@@ -188,8 +187,8 @@ describe("TimeSeries", function() {
       });
 
       it('should set fill zero, and enable points', function() {
-        expect(series.lines.fill).to.be(0.001);
-        expect(series.points.show).to.be(true);
+        expect(series.lines.fill).toBe(0.001);
+        expect(series.points.show).toBe(true);
       });
     });
 
@@ -200,8 +199,8 @@ describe("TimeSeries", function() {
       });
 
       it('should disable lines, and enable bars', function() {
-        expect(series.lines.show).to.be(false);
-        expect(series.bars.show).to.be(true);
+        expect(series.lines.show).toBe(false);
+        expect(series.bars.show).toBe(true);
       });
     });
 
@@ -212,8 +211,8 @@ describe("TimeSeries", function() {
       });
 
       it('should disable stack, and set lineWidth', function() {
-        expect(series.stack).to.be(false);
-        expect(series.lines.lineWidth).to.be(5);
+        expect(series.stack).toBe(false);
+        expect(series.lines.lineWidth).toBe(5);
       });
     });
 
@@ -224,9 +223,9 @@ describe("TimeSeries", function() {
       });
 
       it('should enable dashes, set dashes lineWidth to 5 and lines lineWidth to 0', function() {
-        expect(series.dashes.show).to.be(true);
-        expect(series.dashes.lineWidth).to.be(5);
-        expect(series.lines.lineWidth).to.be(0);
+        expect(series.dashes.show).toBe(true);
+        expect(series.dashes.lineWidth).toBe(5);
+        expect(series.lines.lineWidth).toBe(0);
       });
     });
 
@@ -237,7 +236,7 @@ describe("TimeSeries", function() {
       });
 
       it('should disable line fill and add fillBelowTo', function() {
-        expect(series.fillBelowTo).to.be('min');
+        expect(series.fillBelowTo).toBe('min');
       });
     });
 
@@ -248,8 +247,8 @@ describe("TimeSeries", function() {
       });
 
       it('should set pointradius, and set steppedLine', function() {
-        expect(series.points.radius).to.be(5);
-        expect(series.lines.steps).to.be(true);
+        expect(series.points.radius).toBe(5);
+        expect(series.lines.steps).toBe(true);
       });
     });
 
@@ -260,7 +259,7 @@ describe("TimeSeries", function() {
       });
 
       it('should match second series', function() {
-        expect(series.lines.show).to.be(false);
+        expect(series.lines.show).toBe(false);
       });
     });
 
@@ -271,11 +270,11 @@ describe("TimeSeries", function() {
       });
 
       it('should set yaxis', function() {
-        expect(series.yaxis).to.be(2);
+        expect(series.yaxis).toBe(2);
       });
 
       it('should set zindex', function() {
-        expect(series.zindex).to.be(2);
+        expect(series.zindex).toBe(2);
       });
     });
 
@@ -288,11 +287,11 @@ describe("TimeSeries", function() {
     });
 
     it('should format non-numeric values as empty string', function() {
-      expect(series.formatValue(null)).to.be("");
-      expect(series.formatValue(undefined)).to.be("");
-      expect(series.formatValue(NaN)).to.be("");
-      expect(series.formatValue(Infinity)).to.be("");
-      expect(series.formatValue(-Infinity)).to.be("");
+      expect(series.formatValue(null)).toBe("");
+      expect(series.formatValue(undefined)).toBe("");
+      expect(series.formatValue(NaN)).toBe("");
+      expect(series.formatValue(Infinity)).toBe("");
+      expect(series.formatValue(-Infinity)).toBe("");
     });
   });
 

+ 0 - 7
public/app/core/time_series.js

@@ -1,7 +0,0 @@
-define([
-  './time_series2'
-], function(timeSeries) {
-  'use strict';
-  // backward compatability hack;
-  return timeSeries.default;
-});

+ 0 - 2
public/app/core/components/PasswordStrength.tsx → public/app/core/ui/PasswordStrength.tsx

@@ -1,5 +1,4 @@
 import React from 'react';
-import { react2AngularDirective } from 'app/core/utils/react2angular';
 
 export interface IProps {
   password: string;
@@ -33,5 +32,4 @@ export class PasswordStrength extends React.Component<IProps, any> {
   }
 }
 
-react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
 

+ 0 - 936
public/app/core/utils/kbn.js

@@ -1,936 +0,0 @@
-define([
-  'jquery',
-  'lodash',
-  'moment'
-],
-function($, _, moment) {
-  'use strict';
-
-  var kbn = {};
-  kbn.valueFormats = {};
-
-  kbn.regexEscape = function(value) {
-    return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
-  };
-
-  ///// HELPER FUNCTIONS /////
-
-  kbn.round_interval = function(interval) {
-    switch (true) {
-      // 0.015s
-      case (interval < 15):
-        return 10;      // 0.01s
-      // 0.035s
-      case (interval < 35):
-        return 20;      // 0.02s
-      // 0.075s
-      case (interval < 75):
-        return 50;       // 0.05s
-      // 0.15s
-      case (interval < 150):
-        return 100;      // 0.1s
-      // 0.35s
-      case (interval < 350):
-        return 200;      // 0.2s
-      // 0.75s
-      case (interval < 750):
-        return 500;       // 0.5s
-      // 1.5s
-      case (interval < 1500):
-        return 1000;      // 1s
-      // 3.5s
-      case (interval < 3500):
-        return 2000;      // 2s
-      // 7.5s
-      case (interval < 7500):
-        return 5000;      // 5s
-      // 12.5s
-      case (interval < 12500):
-        return 10000;     // 10s
-      // 17.5s
-      case (interval < 17500):
-        return 15000;     // 15s
-      // 25s
-      case (interval < 25000):
-        return 20000;     // 20s
-      // 45s
-      case (interval < 45000):
-        return 30000;     // 30s
-      // 1.5m
-      case (interval < 90000):
-        return 60000;     // 1m
-      // 3.5m
-      case (interval < 210000):
-        return 120000;    // 2m
-      // 7.5m
-      case (interval < 450000):
-        return 300000;    // 5m
-      // 12.5m
-      case (interval < 750000):
-        return 600000;    // 10m
-      // 12.5m
-      case (interval < 1050000):
-        return 900000;    // 15m
-      // 25m
-      case (interval < 1500000):
-        return 1200000;   // 20m
-      // 45m
-      case (interval < 2700000):
-        return 1800000;   // 30m
-      // 1.5h
-      case (interval < 5400000):
-        return 3600000;   // 1h
-      // 2.5h
-      case (interval < 9000000):
-        return 7200000;   // 2h
-      // 4.5h
-      case (interval < 16200000):
-        return 10800000;  // 3h
-      // 9h
-      case (interval < 32400000):
-        return 21600000;  // 6h
-      // 1d
-      case (interval < 86400000):
-        return 43200000;  // 12h
-      // 1w
-      case (interval < 604800000):
-        return 86400000;  // 1d
-      // 3w
-      case (interval < 1814400000):
-        return 604800000; // 1w
-      // 6w
-      case (interval < 3628800000):
-        return 2592000000; // 30d
-      default:
-        return 31536000000; // 1y
-    }
-  };
-
-  kbn.secondsToHms = function(seconds) {
-    var numyears = Math.floor(seconds / 31536000);
-    if(numyears){
-      return numyears + 'y';
-    }
-    var numdays = Math.floor((seconds % 31536000) / 86400);
-    if(numdays){
-      return numdays + 'd';
-    }
-    var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
-    if(numhours){
-      return numhours + 'h';
-    }
-    var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
-    if(numminutes){
-      return numminutes + 'm';
-    }
-    var numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60);
-    if(numseconds){
-      return numseconds + 's';
-    }
-    var nummilliseconds = Math.floor(seconds * 1000.0);
-    if(nummilliseconds){
-      return nummilliseconds + 'ms';
-    }
-
-    return 'less than a millisecond'; //'just now' //or other string you like;
-  };
-
-  kbn.to_percent = function(number,outof) {
-    return Math.floor((number/outof)*10000)/100 + "%";
-  };
-
-  kbn.addslashes = function(str) {
-    str = str.replace(/\\/g, '\\\\');
-    str = str.replace(/\'/g, '\\\'');
-    str = str.replace(/\"/g, '\\"');
-    str = str.replace(/\0/g, '\\0');
-    return str;
-  };
-
-  kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
-
-  // histogram & trends
-  kbn.intervals_in_seconds = {
-    y: 31536000,
-    M: 2592000,
-    w: 604800,
-    d: 86400,
-    h: 3600,
-    m: 60,
-    s: 1,
-    ms: 0.001
-  };
-
-  kbn.calculateInterval = function(range, resolution, lowLimitInterval) {
-    var lowLimitMs = 1; // 1 millisecond default low limit
-    var intervalMs;
-
-    if (lowLimitInterval) {
-      if (lowLimitInterval[0] === '>') {
-        lowLimitInterval = lowLimitInterval.slice(1);
-      }
-      lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
-    }
-
-    intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
-    if (lowLimitMs > intervalMs) {
-      intervalMs = lowLimitMs;
-    }
-
-    return {
-      intervalMs: intervalMs,
-      interval: kbn.secondsToHms(intervalMs / 1000),
-    };
-  };
-
-  kbn.describe_interval = function (string) {
-    var matches = string.match(kbn.interval_regex);
-    if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
-      throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"');
-    } else {
-      return {
-        sec: kbn.intervals_in_seconds[matches[2]],
-        type: matches[2],
-        count: parseInt(matches[1], 10)
-      };
-    }
-  };
-
-  kbn.interval_to_ms = function(string) {
-    var info = kbn.describe_interval(string);
-    return info.sec * 1000 * info.count;
-  };
-
-  kbn.interval_to_seconds = function (string) {
-    var info = kbn.describe_interval(string);
-    return info.sec * info.count;
-  };
-
-  kbn.query_color_dot = function (color, diameter) {
-    return '<div class="icon-circle" style="' + [
-      'display:inline-block',
-      'color:' + color,
-      'font-size:' + diameter + 'px',
-    ].join(';') + '"></div>';
-  };
-
-  kbn.slugifyForUrl = function(str) {
-    return str
-      .toLowerCase()
-      .replace(/[^\w ]+/g,'')
-      .replace(/ +/g,'-');
-  };
-
-  kbn.stringToJsRegex = function(str) {
-    if (str[0] !== '/') {
-      return new RegExp('^' + str + '$');
-    }
-
-    var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
-    return new RegExp(match[1], match[2]);
-  };
-
-  kbn.toFixed = function(value, decimals) {
-    if (value === null) {
-      return "";
-    }
-
-    var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
-    var formatted = String(Math.round(value * factor) / factor);
-
-    // if exponent return directly
-    if (formatted.indexOf('e') !== -1 || value === 0) {
-      return formatted;
-    }
-
-    // If tickDecimals was specified, ensure that we have exactly that
-    // much precision; otherwise default to the value's own precision.
-    if (decimals != null) {
-      var decimalPos = formatted.indexOf(".");
-      var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
-      if (precision < decimals) {
-        return (precision ? formatted : formatted + ".") + (String(factor)).substr(1, decimals - precision);
-      }
-    }
-
-    return formatted;
-  };
-
-  kbn.toFixedScaled = function(value, decimals, scaledDecimals, additionalDecimals, ext) {
-    if (scaledDecimals === null) {
-      return kbn.toFixed(value, decimals) + ext;
-    } else {
-      return kbn.toFixed(value, scaledDecimals + additionalDecimals) + ext;
-    }
-  };
-
-  kbn.roundValue = function (num, decimals) {
-    if (num === null) { return null; }
-    var n = Math.pow(10, decimals);
-    return Math.round((n * num).toFixed(decimals))  / n;
-  };
-
-  ///// FORMAT FUNCTION CONSTRUCTORS /////
-
-  kbn.formatBuilders = {};
-
-  // Formatter which always appends a fixed unit string to the value. No
-  // scaling of the value is performed.
-  kbn.formatBuilders.fixedUnit = function(unit) {
-    return function(size, decimals) {
-      if (size === null) { return ""; }
-      return kbn.toFixed(size, decimals) + ' ' + unit;
-    };
-  };
-
-  // Formatter which scales the unit string geometrically according to the given
-  // numeric factor. Repeatedly scales the value down by the factor until it is
-  // less than the factor in magnitude, or the end of the array is reached.
-  kbn.formatBuilders.scaledUnits = function(factor, extArray) {
-    return function(size, decimals, scaledDecimals) {
-      if (size === null) {
-        return "";
-      }
-
-      var steps = 0;
-      var limit = extArray.length;
-
-      while (Math.abs(size) >= factor) {
-        steps++;
-        size /= factor;
-
-        if (steps >= limit) { return "NA"; }
-      }
-
-      if (steps > 0 && scaledDecimals !== null) {
-        decimals = scaledDecimals + (3 * steps);
-      }
-
-      return kbn.toFixed(size, decimals) + extArray[steps];
-    };
-  };
-
-  // Extension of the scaledUnits builder which uses SI decimal prefixes. If an
-  // offset is given, it adjusts the starting units at the given prefix; a value
-  // of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc.
-  kbn.formatBuilders.decimalSIPrefix = function(unit, offset) {
-    var prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
-    prefixes = prefixes.slice(3 + (offset || 0));
-    var units = prefixes.map(function(p) { return ' ' + p + unit; });
-    return kbn.formatBuilders.scaledUnits(1000, units);
-  };
-
-  // Extension of the scaledUnits builder which uses SI binary prefixes. If
-  // offset is given, it starts the units at the given prefix; otherwise, the
-  // offset defaults to zero and the initial unit is not prefixed.
-  kbn.formatBuilders.binarySIPrefix = function(unit, offset) {
-    var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
-    var units = prefixes.map(function(p) { return ' ' + p + unit; });
-    return kbn.formatBuilders.scaledUnits(1024, units);
-  };
-
-  // Currency formatter for prefixing a symbol onto a number. Supports scaling
-  // up to the trillions.
-  kbn.formatBuilders.currency = function(symbol) {
-    var units = ['', 'K', 'M', 'B', 'T'];
-    var scaler = kbn.formatBuilders.scaledUnits(1000, units);
-    return function(size, decimals, scaledDecimals) {
-      if (size === null) { return ""; }
-      var scaled = scaler(size, decimals, scaledDecimals);
-      return symbol + scaled;
-    };
-  };
-
-  kbn.formatBuilders.simpleCountUnit = function(symbol) {
-    var units = ['', 'K', 'M', 'B', 'T'];
-    var scaler = kbn.formatBuilders.scaledUnits(1000, units);
-    return function(size, decimals, scaledDecimals) {
-      if (size === null) { return ""; }
-      var scaled = scaler(size, decimals, scaledDecimals);
-      return scaled + " " + symbol;
-    };
-  };
-
-  ///// VALUE FORMATS /////
-
-  // Dimensionless Units
-  kbn.valueFormats.none  = kbn.toFixed;
-  kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, ['', ' K', ' Mil', ' Bil', ' Tri', ' Quadr', ' Quint', ' Sext', ' Sept']);
-  kbn.valueFormats.dB    = kbn.formatBuilders.fixedUnit('dB');
-  kbn.valueFormats.ppm   = kbn.formatBuilders.fixedUnit('ppm');
-
-  kbn.valueFormats.percent = function(size, decimals) {
-    if (size === null) { return ""; }
-    return kbn.toFixed(size, decimals) + '%';
-  };
-
-  kbn.valueFormats.percentunit = function(size, decimals) {
-    if (size === null) { return ""; }
-    return kbn.toFixed(100*size, decimals) + '%';
-  };
-
-  /* Formats the value to hex. Uses float if specified decimals are not 0.
-   * There are two options, one with 0x, and one without */
-
-  kbn.valueFormats.hex = function(value, decimals) {
-    if (value == null) { return ""; }
-    return parseFloat(kbn.toFixed(value, decimals)).toString(16).toUpperCase();
-  };
-
-  kbn.valueFormats.hex0x = function(value, decimals) {
-    if (value == null) { return ""; }
-    var hexString = kbn.valueFormats.hex(value, decimals);
-    if (hexString.substring(0,1) === "-") {
-      return "-0x" + hexString.substring(1);
-    }
-    return "0x" + hexString;
-  };
-
-  kbn.valueFormats.sci = function(value, decimals) {
-    return value.toExponential(decimals);
-  };
-
-  kbn.valueFormats.locale = function(value, decimals) {
-    return value.toLocaleString(undefined, {maximumFractionDigits: decimals});
-  };
-
-  // Currencies
-  kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
-  kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
-  kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
-  kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
-  kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
-  kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴');
-  kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$');
-  kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
-  kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
-  kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
-  kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
-
-  // Data (Binary)
-  kbn.valueFormats.bits   = kbn.formatBuilders.binarySIPrefix('b');
-  kbn.valueFormats.bytes  = kbn.formatBuilders.binarySIPrefix('B');
-  kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1);
-  kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2);
-  kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3);
-
-  // Data (Decimal)
-  kbn.valueFormats.decbits   = kbn.formatBuilders.decimalSIPrefix('b');
-  kbn.valueFormats.decbytes  = kbn.formatBuilders.decimalSIPrefix('B');
-  kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1);
-  kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2);
-  kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3);
-
-  // Data Rate
-  kbn.valueFormats.pps    = kbn.formatBuilders.decimalSIPrefix('pps');
-  kbn.valueFormats.bps    = kbn.formatBuilders.decimalSIPrefix('bps');
-  kbn.valueFormats.Bps    = kbn.formatBuilders.decimalSIPrefix('Bps');
-  kbn.valueFormats.KBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
-  kbn.valueFormats.Kbits  = kbn.formatBuilders.decimalSIPrefix('bps', 1);
-  kbn.valueFormats.MBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
-  kbn.valueFormats.Mbits  = kbn.formatBuilders.decimalSIPrefix('bps', 2);
-  kbn.valueFormats.GBs    = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
-  kbn.valueFormats.Gbits  = kbn.formatBuilders.decimalSIPrefix('bps', 3);
-
-  // Throughput
-  kbn.valueFormats.ops  = kbn.formatBuilders.simpleCountUnit('ops');
-  kbn.valueFormats.rps  = kbn.formatBuilders.simpleCountUnit('rps');
-  kbn.valueFormats.wps  = kbn.formatBuilders.simpleCountUnit('wps');
-  kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops');
-  kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm');
-  kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm');
-  kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm');
-
-  // Energy
-  kbn.valueFormats.watt         = kbn.formatBuilders.decimalSIPrefix('W');
-  kbn.valueFormats.kwatt        = kbn.formatBuilders.decimalSIPrefix('W', 1);
-  kbn.valueFormats.voltamp      = kbn.formatBuilders.decimalSIPrefix('VA');
-  kbn.valueFormats.kvoltamp     = kbn.formatBuilders.decimalSIPrefix('VA', 1);
-  kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
-  kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1);
-  kbn.valueFormats.watth        = kbn.formatBuilders.decimalSIPrefix('Wh');
-  kbn.valueFormats.kwatth       = kbn.formatBuilders.decimalSIPrefix('Wh', 1);
-  kbn.valueFormats.joule        = kbn.formatBuilders.decimalSIPrefix('J');
-  kbn.valueFormats.ev           = kbn.formatBuilders.decimalSIPrefix('eV');
-  kbn.valueFormats.amp          = kbn.formatBuilders.decimalSIPrefix('A');
-  kbn.valueFormats.kamp         = kbn.formatBuilders.decimalSIPrefix('A', 1);
-  kbn.valueFormats.volt         = kbn.formatBuilders.decimalSIPrefix('V');
-  kbn.valueFormats.kvolt        = kbn.formatBuilders.decimalSIPrefix('V', 1);
-  kbn.valueFormats.dBm          = kbn.formatBuilders.decimalSIPrefix('dBm');
-
-  // Temperature
-  kbn.valueFormats.celsius   = kbn.formatBuilders.fixedUnit('°C');
-  kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F');
-  kbn.valueFormats.kelvin    = kbn.formatBuilders.fixedUnit('K');
-  kbn.valueFormats.humidity  = kbn.formatBuilders.fixedUnit('%H');
-
-  // Pressure
-  kbn.valueFormats.pressurebar  = kbn.formatBuilders.decimalSIPrefix('bar');
-  kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1);
-  kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
-  kbn.valueFormats.pressurehpa  = kbn.formatBuilders.fixedUnit('hPa');
-  kbn.valueFormats.pressurehg   = kbn.formatBuilders.fixedUnit('"Hg');
-  kbn.valueFormats.pressurepsi  = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']);
-
-  // Force
-  kbn.valueFormats.forceNm  = kbn.formatBuilders.decimalSIPrefix('Nm');
-  kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1);
-  kbn.valueFormats.forceN   = kbn.formatBuilders.decimalSIPrefix('N');
-  kbn.valueFormats.forcekN  = kbn.formatBuilders.decimalSIPrefix('N', 1);
-
-  // Length
-  kbn.valueFormats.lengthm  = kbn.formatBuilders.decimalSIPrefix('m');
-  kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1);
-  kbn.valueFormats.lengthkm = kbn.formatBuilders.decimalSIPrefix('m', 1);
-  kbn.valueFormats.lengthmi = kbn.formatBuilders.fixedUnit('mi');
-
-  // Mass
-  kbn.valueFormats.massmg  = kbn.formatBuilders.decimalSIPrefix('g', -1);
-  kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g');
-  kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1);
-  kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t');
-
-  // Velocity
-  kbn.valueFormats.velocityms   = kbn.formatBuilders.fixedUnit('m/s');
-  kbn.valueFormats.velocitykmh  = kbn.formatBuilders.fixedUnit('km/h');
-  kbn.valueFormats.velocitymph  = kbn.formatBuilders.fixedUnit('mph');
-  kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn');
-
-  // Volume
-  kbn.valueFormats.litre  = kbn.formatBuilders.decimalSIPrefix('L');
-  kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
-  kbn.valueFormats.m3     = kbn.formatBuilders.decimalSIPrefix('m3');
-  kbn.valueFormats.dm3    = kbn.formatBuilders.decimalSIPrefix('dm3');
-  kbn.valueFormats.gallons  = kbn.formatBuilders.fixedUnit('gal');
-
-  // Flow
-  kbn.valueFormats.flowgpm  = kbn.formatBuilders.fixedUnit('gpm');
-  kbn.valueFormats.flowcms  = kbn.formatBuilders.fixedUnit('cms');
-  kbn.valueFormats.flowcfs  = kbn.formatBuilders.fixedUnit('cfs');
-  kbn.valueFormats.flowcfm  = kbn.formatBuilders.fixedUnit('cfm');
-
-  // Time
-  kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
-
-  kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 1000) {
-      return kbn.toFixed(size, decimals) + " ms";
-    }
-    // Less than 1 min
-    else if (Math.abs(size) < 60000) {
-      return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " s");
-    }
-    // Less than 1 hour, devide in minutes
-    else if (Math.abs(size) < 3600000) {
-      return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, " min");
-    }
-    // Less than one day, devide in hours
-    else if (Math.abs(size) < 86400000) {
-      return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, " hour");
-    }
-    // Less than one year, devide in days
-    else if (Math.abs(size) < 31536000000) {
-      return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, " day");
-    }
-
-    return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, " year");
-  };
-
-  kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    // Less than 1 µs, devide in ns
-    if (Math.abs(size) < 0.000001) {
-      return kbn.toFixedScaled(size * 1.e9, decimals, scaledDecimals - decimals, -9, " ns");
-    }
-    // Less than 1 ms, devide in µs
-    if (Math.abs(size) < 0.001) {
-      return kbn.toFixedScaled(size * 1.e6, decimals, scaledDecimals - decimals, -6, " µs");
-    }
-    // Less than 1 second, devide in ms
-    if (Math.abs(size) < 1) {
-      return kbn.toFixedScaled(size * 1.e3, decimals, scaledDecimals - decimals, -3, " ms");
-    }
-
-    if (Math.abs(size) < 60) {
-      return kbn.toFixed(size, decimals) + " s";
-    }
-    // Less than 1 hour, devide in minutes
-    else if (Math.abs(size) < 3600) {
-      return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, " min");
-    }
-    // Less than one day, devide in hours
-    else if (Math.abs(size) < 86400) {
-      return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, " hour");
-    }
-    // Less than one week, devide in days
-    else if (Math.abs(size) < 604800) {
-      return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, " day");
-    }
-    // Less than one year, devide in week
-    else if (Math.abs(size) < 31536000) {
-      return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, " week");
-    }
-
-    return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, " year");
-  };
-
-  kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 1000) {
-      return kbn.toFixed(size, decimals) + " µs";
-    }
-    else if (Math.abs(size) < 1000000) {
-      return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " ms");
-    }
-    else {
-      return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, " s");
-    }
-  };
-
-  kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 1000) {
-      return kbn.toFixed(size, decimals) + " ns";
-    }
-    else if (Math.abs(size) < 1000000) {
-      return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, " µs");
-    }
-    else if (Math.abs(size) < 1000000000) {
-      return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, " ms");
-    }
-    else if (Math.abs(size) < 60000000000){
-      return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, " s");
-    }
-    else {
-      return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, " min");
-    }
-  };
-
-  kbn.valueFormats.m = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 60) {
-      return kbn.toFixed(size, decimals) + " min";
-    }
-    else if (Math.abs(size) < 1440) {
-      return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, " hour");
-    }
-    else if (Math.abs(size) < 10080) {
-      return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, " day");
-    }
-    else if (Math.abs(size) < 604800) {
-      return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, " week");
-    }
-    else {
-      return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, " year");
-    }
-  };
-
-  kbn.valueFormats.h = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 24) {
-      return kbn.toFixed(size, decimals) + " hour";
-    }
-    else if (Math.abs(size) < 168) {
-      return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, " day");
-    }
-    else if (Math.abs(size) < 8760) {
-      return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, " week");
-    }
-    else {
-      return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, " year");
-    }
-  };
-
-  kbn.valueFormats.d = function(size, decimals, scaledDecimals) {
-    if (size === null) { return ""; }
-
-    if (Math.abs(size) < 7) {
-      return kbn.toFixed(size, decimals) + " day";
-    }
-    else if (Math.abs(size) < 365) {
-      return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, " week");
-    }
-    else {
-      return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, " year");
-    }
-  };
-
-  kbn.toDuration = function(size, decimals, timeScale) {
-    if (size === null) { return ""; }
-    if (size === 0) { return "0 " + timeScale + "s"; }
-    if (size < 0) { return kbn.toDuration(-size, decimals, timeScale) + " ago"; }
-
-    var units = [
-      {short: "y",  long: "year"},
-      {short: "M",  long: "month"},
-      {short: "w",  long: "week"},
-      {short: "d",  long: "day"},
-      {short: "h",  long: "hour"},
-      {short: "m",  long: "minute"},
-      {short: "s",  long: "second"},
-      {short: "ms", long: "millisecond"}
-    ];
-    // convert $size to milliseconds
-    // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
-    size *= kbn.intervals_in_seconds[units.find(function(e) { return e.long === timeScale; }).short] * 1000;
-
-    var string = [];
-    // after first value >= 1 print only $decimals more
-    var decrementDecimals = false;
-    for (var i = 0; i < units.length && decimals >= 0; i++) {
-      var interval = kbn.intervals_in_seconds[units[i].short] * 1000;
-      var value = size / interval;
-      if (value >= 1 || decrementDecimals) {
-        decrementDecimals = true;
-        var floor = Math.floor(value);
-        var unit = units[i].long + (floor !== 1 ? "s" : "");
-        string.push(floor + " " + unit);
-        size = size % interval;
-        decimals--;
-      }
-    }
-
-    return string.join(", ");
-  };
-
-  kbn.valueFormats.dtdurationms = function(size, decimals) {
-    return kbn.toDuration(size, decimals, 'millisecond');
-  };
-
-  kbn.valueFormats.dtdurations = function(size, decimals) {
-    return kbn.toDuration(size, decimals, 'second');
-  };
-
-  kbn.valueFormats.dateTimeAsIso = function(epoch) {
-    var time = moment(epoch);
-
-    if (moment().isSame(epoch, 'day')) {
-      return time.format('HH:mm:ss');
-    }
-    return time.format('YYYY-MM-DD HH:mm:ss');
-  };
-
-  kbn.valueFormats.dateTimeAsUS = function(epoch) {
-    var time = moment(epoch);
-
-    if (moment().isSame(epoch, 'day')) {
-      return time.format('h:mm:ss a');
-    }
-    return time.format('MM/DD/YYYY h:mm:ss a');
-  };
-
-  kbn.valueFormats.dateTimeFromNow = function(epoch) {
-    return moment(epoch).fromNow();
-  };
-
-  ///// FORMAT MENU /////
-
-  kbn.getUnitFormats = function() {
-    return [
-      {
-        text: 'none',
-        submenu: [
-          {text: 'none' ,               value: 'none'       },
-          {text: 'short',               value: 'short'      },
-          {text: 'percent (0-100)',     value: 'percent'    },
-          {text: 'percent (0.0-1.0)',   value: 'percentunit'},
-          {text: 'Humidity (%H)',       value: 'humidity'   },
-          {text: 'ppm',                 value: 'ppm'        },
-          {text: 'decibel',             value: 'dB'         },
-          {text: 'hexadecimal (0x)',    value: 'hex0x'      },
-          {text: 'hexadecimal',         value: 'hex'        },
-          {text: 'scientific notation', value: 'sci'        },
-          {text: 'locale format',       value: 'locale'     },
-        ]
-      },
-      {
-        text: 'currency',
-        submenu: [
-          {text: 'Dollars ($)', value: 'currencyUSD'},
-          {text: 'Pounds (£)',  value: 'currencyGBP'},
-          {text: 'Euro (€)',    value: 'currencyEUR'},
-          {text: 'Yen (¥)',     value: 'currencyJPY'},
-          {text: 'Rubles (₽)',  value: 'currencyRUB'},
-          {text: 'Hryvnias (₴)',  value: 'currencyUAH'},
-          {text: 'Real (R$)', value: 'currencyBRL'},
-          {text: 'Danish Krone (kr)', value: 'currencyDKK'},
-          {text: 'Icelandic Krone (kr)', value: 'currencyISK'},
-          {text: 'Norwegian Krone (kr)', value: 'currencyNOK'},
-          {text: 'Swedish Krone (kr)', value: 'currencySEK'},
-        ]
-      },
-      {
-        text: 'time',
-        submenu: [
-          {text: 'Hertz (1/s)',       value: 'hertz'},
-          {text: 'nanoseconds (ns)' , value: 'ns'   },
-          {text: 'microseconds (µs)', value: 'µs'   },
-          {text: 'milliseconds (ms)', value: 'ms'   },
-          {text: 'seconds (s)',       value: 's'    },
-          {text: 'minutes (m)',       value: 'm'    },
-          {text: 'hours (h)',         value: 'h'    },
-          {text: 'days (d)',          value: 'd'    },
-          {text: 'duration (ms)',     value: 'dtdurationms' },
-          {text: 'duration (s)',      value: 'dtdurations' },
-        ]
-      },
-      {
-        text: 'date & time',
-        submenu: [
-          {text: 'YYYY-MM-DD HH:mm:ss',   value: 'dateTimeAsIso' },
-          {text: 'DD/MM/YYYY h:mm:ss a',  value: 'dateTimeAsUS' },
-          {text: 'From Now',              value: 'dateTimeFromNow' },
-        ]
-      },
-      {
-        text: 'data (IEC)',
-        submenu: [
-          {text: 'bits',      value: 'bits'  },
-          {text: 'bytes',     value: 'bytes' },
-          {text: 'kibibytes', value: 'kbytes'},
-          {text: 'mebibytes', value: 'mbytes'},
-          {text: 'gibibytes', value: 'gbytes'},
-        ]
-      },
-      {
-        text: 'data (Metric)',
-        submenu: [
-          {text: 'bits',      value: 'decbits'  },
-          {text: 'bytes',     value: 'decbytes' },
-          {text: 'kilobytes', value: 'deckbytes'},
-          {text: 'megabytes', value: 'decmbytes'},
-          {text: 'gigabytes', value: 'decgbytes'},
-        ]
-      },
-      {
-        text: 'data rate',
-        submenu: [
-          {text: 'packets/sec', value: 'pps'},
-          {text: 'bits/sec',    value: 'bps'},
-          {text: 'bytes/sec',   value: 'Bps'},
-          {text: 'kilobits/sec', value: 'Kbits'},
-          {text: 'kilobytes/sec',    value: 'KBs'},
-          {text: 'megabits/sec', value: 'Mbits'},
-          {text: 'megabytes/sec',    value: 'MBs'},
-          {text: 'gigabytes/sec',   value: 'GBs'},
-          {text: 'gigabits/sec',   value: 'Gbits'},
-        ]
-      },
-      {
-        text: 'throughput',
-        submenu: [
-          {text: 'ops/sec (ops)',       value: 'ops' },
-          {text: 'reads/sec (rps)',     value: 'rps' },
-          {text: 'writes/sec (wps)',    value: 'wps' },
-          {text: 'I/O ops/sec (iops)',  value: 'iops'},
-          {text: 'ops/min (opm)',       value: 'opm' },
-          {text: 'reads/min (rpm)',     value: 'rpm' },
-          {text: 'writes/min (wpm)',    value: 'wpm' },
-        ]
-      },
-      {
-        text: 'length',
-        submenu: [
-          {text: 'millimetre (mm)', value: 'lengthmm'},
-          {text: 'meter (m)',       value: 'lengthm' },
-          {text: 'kilometer (km)',  value: 'lengthkm'},
-          {text: 'mile (mi)',       value: 'lengthmi'},
-        ]
-      },
-      {
-        text: 'mass',
-        submenu: [
-          {text: 'milligram (mg)', value: 'massmg'},
-          {text: 'gram (g)',       value: 'massg' },
-          {text: 'kilogram (kg)',  value: 'masskg'},
-          {text: 'metric ton (t)', value: 'masst' },
-        ]
-      },
-      {
-        text: 'velocity',
-        submenu: [
-          {text: 'm/s',       value: 'velocityms'  },
-          {text: 'km/h',      value: 'velocitykmh' },
-          {text: 'mph',       value: 'velocitymph' },
-          {text: 'knot (kn)', value: 'velocityknot'},
-        ]
-      },
-      {
-        text: 'volume',
-        submenu: [
-          {text: 'millilitre',      value: 'mlitre' },
-          {text: 'litre',           value: 'litre'  },
-          {text: 'cubic metre',     value: 'm3'     },
-          {text: 'cubic decimetre', value: 'dm3'    },
-          {text: 'gallons',         value: 'gallons'},
-        ]
-      },
-      {
-        text: 'energy',
-        submenu: [
-          {text: 'watt (W)',                   value: 'watt'        },
-          {text: 'kilowatt (kW)',              value: 'kwatt'       },
-          {text: 'volt-ampere (VA)',           value: 'voltamp'     },
-          {text: 'kilovolt-ampere (kVA)',      value: 'kvoltamp'    },
-          {text: 'volt-ampere reactive (var)', value: 'voltampreact'},
-          {text: 'kilovolt-ampere reactive (kvar)', value: 'kvoltampreact'},
-          {text: 'watt-hour (Wh)',             value: 'watth'       },
-          {text: 'kilowatt-hour (kWh)',        value: 'kwatth'      },
-          {text: 'joule (J)',                  value: 'joule'       },
-          {text: 'electron volt (eV)',         value: 'ev'          },
-          {text: 'Ampere (A)',                 value: 'amp'         },
-          {text: 'Kiloampere (kA)',            value: 'kamp'        },
-          {text: 'Volt (V)',                   value: 'volt'        },
-          {text: 'Kilovolt (kV)',              value: 'kvolt'       },
-          {text: 'Decibel-milliwatt (dBm)',    value: 'dBm'         },
-        ]
-      },
-      {
-        text: 'temperature',
-        submenu: [
-          {text: 'Celsius (°C)',    value: 'celsius'     },
-          {text: 'Farenheit (°F)',  value: 'farenheit'   },
-          {text: 'Kelvin (K)',      value: 'kelvin'      },
-        ]
-      },
-      {
-        text: 'pressure',
-        submenu: [
-          {text: 'Millibars',         value: 'pressurembar'},
-          {text: 'Bars',              value: 'pressurebar' },
-          {text: 'Kilobars',          value: 'pressurekbar'},
-          {text: 'Hectopascals',      value: 'pressurehpa' },
-          {text: 'Inches of mercury', value: 'pressurehg'  },
-          {text: 'PSI',               value: 'pressurepsi' },
-        ]
-      },
-      {
-        text: 'force',
-        submenu: [
-          {text: 'Newton-meters (Nm)',      value: 'forceNm'  },
-          {text: 'Kilonewton-meters (kNm)', value: 'forcekNm' },
-          {text: 'Newtons (N)',             value: 'forceN'   },
-          {text: 'Kilonewtons (kN)',        value: 'forcekN'  },
-        ]
-      },
-      {
-        text: 'flow',
-        submenu: [
-          {text: 'Gallons/min (gpm)',       value: 'flowgpm'  },
-          {text: 'Cubic meters/sec (cms)',  value: 'flowcms'  },
-          {text: 'Cubic feet/sec (cfs)',    value: 'flowcfs'  },
-          {text: 'Cubic feet/min (cfm)',    value: 'flowcfm'  },
-        ]
-      }
-    ];
-  };
-
-  return kbn;
-});

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

@@ -0,0 +1,968 @@
+import _ from 'lodash';
+import moment from 'moment';
+
+var kbn: any = {};
+
+kbn.valueFormats = {};
+
+kbn.regexEscape = function(value) {
+  return value.replace(/[\\^$*+?.()|[\]{}\/]/g, '\\$&');
+};
+
+///// HELPER FUNCTIONS /////
+
+kbn.round_interval = function(interval) {
+  switch (true) {
+    // 0.015s
+    case interval < 15:
+      return 10; // 0.01s
+    // 0.035s
+    case interval < 35:
+      return 20; // 0.02s
+    // 0.075s
+    case interval < 75:
+      return 50; // 0.05s
+    // 0.15s
+    case interval < 150:
+      return 100; // 0.1s
+    // 0.35s
+    case interval < 350:
+      return 200; // 0.2s
+    // 0.75s
+    case interval < 750:
+      return 500; // 0.5s
+    // 1.5s
+    case interval < 1500:
+      return 1000; // 1s
+    // 3.5s
+    case interval < 3500:
+      return 2000; // 2s
+    // 7.5s
+    case interval < 7500:
+      return 5000; // 5s
+    // 12.5s
+    case interval < 12500:
+      return 10000; // 10s
+    // 17.5s
+    case interval < 17500:
+      return 15000; // 15s
+    // 25s
+    case interval < 25000:
+      return 20000; // 20s
+    // 45s
+    case interval < 45000:
+      return 30000; // 30s
+    // 1.5m
+    case interval < 90000:
+      return 60000; // 1m
+    // 3.5m
+    case interval < 210000:
+      return 120000; // 2m
+    // 7.5m
+    case interval < 450000:
+      return 300000; // 5m
+    // 12.5m
+    case interval < 750000:
+      return 600000; // 10m
+    // 12.5m
+    case interval < 1050000:
+      return 900000; // 15m
+    // 25m
+    case interval < 1500000:
+      return 1200000; // 20m
+    // 45m
+    case interval < 2700000:
+      return 1800000; // 30m
+    // 1.5h
+    case interval < 5400000:
+      return 3600000; // 1h
+    // 2.5h
+    case interval < 9000000:
+      return 7200000; // 2h
+    // 4.5h
+    case interval < 16200000:
+      return 10800000; // 3h
+    // 9h
+    case interval < 32400000:
+      return 21600000; // 6h
+    // 1d
+    case interval < 86400000:
+      return 43200000; // 12h
+    // 1w
+    case interval < 604800000:
+      return 86400000; // 1d
+    // 3w
+    case interval < 1814400000:
+      return 604800000; // 1w
+    // 6w
+    case interval < 3628800000:
+      return 2592000000; // 30d
+    default:
+      return 31536000000; // 1y
+  }
+};
+
+kbn.secondsToHms = function(seconds) {
+  var numyears = Math.floor(seconds / 31536000);
+  if (numyears) {
+    return numyears + 'y';
+  }
+  var numdays = Math.floor((seconds % 31536000) / 86400);
+  if (numdays) {
+    return numdays + 'd';
+  }
+  var numhours = Math.floor(((seconds % 31536000) % 86400) / 3600);
+  if (numhours) {
+    return numhours + 'h';
+  }
+  var numminutes = Math.floor((((seconds % 31536000) % 86400) % 3600) / 60);
+  if (numminutes) {
+    return numminutes + 'm';
+  }
+  var numseconds = Math.floor((((seconds % 31536000) % 86400) % 3600) % 60);
+  if (numseconds) {
+    return numseconds + 's';
+  }
+  var nummilliseconds = Math.floor(seconds * 1000.0);
+  if (nummilliseconds) {
+    return nummilliseconds + 'ms';
+  }
+
+  return 'less than a millisecond'; //'just now' //or other string you like;
+};
+
+kbn.to_percent = function(nr, outof) {
+  return Math.floor(nr / outof * 10000) / 100 + '%';
+};
+
+kbn.addslashes = function(str) {
+  str = str.replace(/\\/g, '\\\\');
+  str = str.replace(/\'/g, "\\'");
+  str = str.replace(/\"/g, '\\"');
+  str = str.replace(/\0/g, '\\0');
+  return str;
+};
+
+kbn.interval_regex = /(\d+(?:\.\d+)?)(ms|[Mwdhmsy])/;
+
+// histogram & trends
+kbn.intervals_in_seconds = {
+  y: 31536000,
+  M: 2592000,
+  w: 604800,
+  d: 86400,
+  h: 3600,
+  m: 60,
+  s: 1,
+  ms: 0.001,
+};
+
+kbn.calculateInterval = function(range, resolution, lowLimitInterval) {
+  var lowLimitMs = 1; // 1 millisecond default low limit
+  var intervalMs;
+
+  if (lowLimitInterval) {
+    if (lowLimitInterval[0] === '>') {
+      lowLimitInterval = lowLimitInterval.slice(1);
+    }
+    lowLimitMs = kbn.interval_to_ms(lowLimitInterval);
+  }
+
+  intervalMs = kbn.round_interval((range.to.valueOf() - range.from.valueOf()) / resolution);
+  if (lowLimitMs > intervalMs) {
+    intervalMs = lowLimitMs;
+  }
+
+  return {
+    intervalMs: intervalMs,
+    interval: kbn.secondsToHms(intervalMs / 1000),
+  };
+};
+
+kbn.describe_interval = function(str) {
+  var matches = str.match(kbn.interval_regex);
+  if (!matches || !_.has(kbn.intervals_in_seconds, matches[2])) {
+    throw new Error('Invalid interval string, expecting a number followed by one of "Mwdhmsy"');
+  } else {
+    return {
+      sec: kbn.intervals_in_seconds[matches[2]],
+      type: matches[2],
+      count: parseInt(matches[1], 10),
+    };
+  }
+};
+
+kbn.interval_to_ms = function(str) {
+  var info = kbn.describe_interval(str);
+  return info.sec * 1000 * info.count;
+};
+
+kbn.interval_to_seconds = function(str) {
+  var info = kbn.describe_interval(str);
+  return info.sec * info.count;
+};
+
+kbn.query_color_dot = function(color, diameter) {
+  return (
+    '<div class="icon-circle" style="' +
+    ['display:inline-block', 'color:' + color, 'font-size:' + diameter + 'px'].join(';') +
+    '"></div>'
+  );
+};
+
+kbn.slugifyForUrl = function(str) {
+  return str
+    .toLowerCase()
+    .replace(/[^\w ]+/g, '')
+    .replace(/ +/g, '-');
+};
+
+kbn.stringToJsRegex = function(str) {
+  if (str[0] !== '/') {
+    return new RegExp('^' + str + '$');
+  }
+
+  var match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$'));
+  return new RegExp(match[1], match[2]);
+};
+
+kbn.toFixed = function(value, decimals) {
+  if (value === null) {
+    return '';
+  }
+
+  var factor = decimals ? Math.pow(10, Math.max(0, decimals)) : 1;
+  var formatted = String(Math.round(value * factor) / factor);
+
+  // if exponent return directly
+  if (formatted.indexOf('e') !== -1 || value === 0) {
+    return formatted;
+  }
+
+  // If tickDecimals was specified, ensure that we have exactly that
+  // much precision; otherwise default to the value's own precision.
+  if (decimals != null) {
+    var decimalPos = formatted.indexOf('.');
+    var precision = decimalPos === -1 ? 0 : formatted.length - decimalPos - 1;
+    if (precision < decimals) {
+      return (precision ? formatted : formatted + '.') + String(factor).substr(1, decimals - precision);
+    }
+  }
+
+  return formatted;
+};
+
+kbn.toFixedScaled = function(value, decimals, scaledDecimals, additionalDecimals, ext) {
+  if (scaledDecimals === null) {
+    return kbn.toFixed(value, decimals) + ext;
+  } else {
+    return kbn.toFixed(value, scaledDecimals + additionalDecimals) + ext;
+  }
+};
+
+kbn.roundValue = function(num, decimals) {
+  if (num === null) {
+    return null;
+  }
+  var n = Math.pow(10, decimals);
+  var formatted = (n * num).toFixed(decimals);
+  return Math.round(parseFloat(formatted)) / n;
+};
+
+///// FORMAT FUNCTION CONSTRUCTORS /////
+
+kbn.formatBuilders = {};
+
+// Formatter which always appends a fixed unit string to the value. No
+// scaling of the value is performed.
+kbn.formatBuilders.fixedUnit = function(unit) {
+  return function(size, decimals) {
+    if (size === null) {
+      return '';
+    }
+    return kbn.toFixed(size, decimals) + ' ' + unit;
+  };
+};
+
+// Formatter which scales the unit string geometrically according to the given
+// numeric factor. Repeatedly scales the value down by the factor until it is
+// less than the factor in magnitude, or the end of the array is reached.
+kbn.formatBuilders.scaledUnits = function(factor, extArray) {
+  return function(size, decimals, scaledDecimals) {
+    if (size === null) {
+      return '';
+    }
+
+    var steps = 0;
+    var limit = extArray.length;
+
+    while (Math.abs(size) >= factor) {
+      steps++;
+      size /= factor;
+
+      if (steps >= limit) {
+        return 'NA';
+      }
+    }
+
+    if (steps > 0 && scaledDecimals !== null) {
+      decimals = scaledDecimals + 3 * steps;
+    }
+
+    return kbn.toFixed(size, decimals) + extArray[steps];
+  };
+};
+
+// Extension of the scaledUnits builder which uses SI decimal prefixes. If an
+// offset is given, it adjusts the starting units at the given prefix; a value
+// of 0 starts at no scale; -3 drops to nano, +2 starts at mega, etc.
+kbn.formatBuilders.decimalSIPrefix = function(unit, offset) {
+  var prefixes = ['n', 'µ', 'm', '', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y'];
+  prefixes = prefixes.slice(3 + (offset || 0));
+  var units = prefixes.map(function(p) {
+    return ' ' + p + unit;
+  });
+  return kbn.formatBuilders.scaledUnits(1000, units);
+};
+
+// Extension of the scaledUnits builder which uses SI binary prefixes. If
+// offset is given, it starts the units at the given prefix; otherwise, the
+// offset defaults to zero and the initial unit is not prefixed.
+kbn.formatBuilders.binarySIPrefix = function(unit, offset) {
+  var prefixes = ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi'].slice(offset);
+  var units = prefixes.map(function(p) {
+    return ' ' + p + unit;
+  });
+  return kbn.formatBuilders.scaledUnits(1024, units);
+};
+
+// Currency formatter for prefixing a symbol onto a number. Supports scaling
+// up to the trillions.
+kbn.formatBuilders.currency = function(symbol) {
+  var units = ['', 'K', 'M', 'B', 'T'];
+  var scaler = kbn.formatBuilders.scaledUnits(1000, units);
+  return function(size, decimals, scaledDecimals) {
+    if (size === null) {
+      return '';
+    }
+    var scaled = scaler(size, decimals, scaledDecimals);
+    return symbol + scaled;
+  };
+};
+
+kbn.formatBuilders.simpleCountUnit = function(symbol) {
+  var units = ['', 'K', 'M', 'B', 'T'];
+  var scaler = kbn.formatBuilders.scaledUnits(1000, units);
+  return function(size, decimals, scaledDecimals) {
+    if (size === null) {
+      return '';
+    }
+    var scaled = scaler(size, decimals, scaledDecimals);
+    return scaled + ' ' + symbol;
+  };
+};
+
+///// VALUE FORMATS /////
+
+// Dimensionless Units
+kbn.valueFormats.none = kbn.toFixed;
+kbn.valueFormats.short = kbn.formatBuilders.scaledUnits(1000, [
+  '',
+  ' K',
+  ' Mil',
+  ' Bil',
+  ' Tri',
+  ' Quadr',
+  ' Quint',
+  ' Sext',
+  ' Sept',
+]);
+kbn.valueFormats.dB = kbn.formatBuilders.fixedUnit('dB');
+kbn.valueFormats.ppm = kbn.formatBuilders.fixedUnit('ppm');
+
+kbn.valueFormats.percent = function(size, decimals) {
+  if (size === null) {
+    return '';
+  }
+  return kbn.toFixed(size, decimals) + '%';
+};
+
+kbn.valueFormats.percentunit = function(size, decimals) {
+  if (size === null) {
+    return '';
+  }
+  return kbn.toFixed(100 * size, decimals) + '%';
+};
+
+/* Formats the value to hex. Uses float if specified decimals are not 0.
+ * There are two options, one with 0x, and one without */
+
+kbn.valueFormats.hex = function(value, decimals) {
+  if (value == null) {
+    return '';
+  }
+  return parseFloat(kbn.toFixed(value, decimals))
+    .toString(16)
+    .toUpperCase();
+};
+
+kbn.valueFormats.hex0x = function(value, decimals) {
+  if (value == null) {
+    return '';
+  }
+  var hexString = kbn.valueFormats.hex(value, decimals);
+  if (hexString.substring(0, 1) === '-') {
+    return '-0x' + hexString.substring(1);
+  }
+  return '0x' + hexString;
+};
+
+kbn.valueFormats.sci = function(value, decimals) {
+  return value.toExponential(decimals);
+};
+
+kbn.valueFormats.locale = function(value, decimals) {
+  return value.toLocaleString(undefined, { maximumFractionDigits: decimals });
+};
+
+// Currencies
+kbn.valueFormats.currencyUSD = kbn.formatBuilders.currency('$');
+kbn.valueFormats.currencyGBP = kbn.formatBuilders.currency('£');
+kbn.valueFormats.currencyEUR = kbn.formatBuilders.currency('€');
+kbn.valueFormats.currencyJPY = kbn.formatBuilders.currency('¥');
+kbn.valueFormats.currencyRUB = kbn.formatBuilders.currency('₽');
+kbn.valueFormats.currencyUAH = kbn.formatBuilders.currency('₴');
+kbn.valueFormats.currencyBRL = kbn.formatBuilders.currency('R$');
+kbn.valueFormats.currencyDKK = kbn.formatBuilders.currency('kr');
+kbn.valueFormats.currencyISK = kbn.formatBuilders.currency('kr');
+kbn.valueFormats.currencyNOK = kbn.formatBuilders.currency('kr');
+kbn.valueFormats.currencySEK = kbn.formatBuilders.currency('kr');
+
+// Data (Binary)
+kbn.valueFormats.bits = kbn.formatBuilders.binarySIPrefix('b');
+kbn.valueFormats.bytes = kbn.formatBuilders.binarySIPrefix('B');
+kbn.valueFormats.kbytes = kbn.formatBuilders.binarySIPrefix('B', 1);
+kbn.valueFormats.mbytes = kbn.formatBuilders.binarySIPrefix('B', 2);
+kbn.valueFormats.gbytes = kbn.formatBuilders.binarySIPrefix('B', 3);
+
+// Data (Decimal)
+kbn.valueFormats.decbits = kbn.formatBuilders.decimalSIPrefix('b');
+kbn.valueFormats.decbytes = kbn.formatBuilders.decimalSIPrefix('B');
+kbn.valueFormats.deckbytes = kbn.formatBuilders.decimalSIPrefix('B', 1);
+kbn.valueFormats.decmbytes = kbn.formatBuilders.decimalSIPrefix('B', 2);
+kbn.valueFormats.decgbytes = kbn.formatBuilders.decimalSIPrefix('B', 3);
+
+// Data Rate
+kbn.valueFormats.pps = kbn.formatBuilders.decimalSIPrefix('pps');
+kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
+kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps');
+kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
+kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1);
+kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
+kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2);
+kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
+kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3);
+
+// Throughput
+kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops');
+kbn.valueFormats.rps = kbn.formatBuilders.simpleCountUnit('rps');
+kbn.valueFormats.wps = kbn.formatBuilders.simpleCountUnit('wps');
+kbn.valueFormats.iops = kbn.formatBuilders.simpleCountUnit('iops');
+kbn.valueFormats.opm = kbn.formatBuilders.simpleCountUnit('opm');
+kbn.valueFormats.rpm = kbn.formatBuilders.simpleCountUnit('rpm');
+kbn.valueFormats.wpm = kbn.formatBuilders.simpleCountUnit('wpm');
+
+// Energy
+kbn.valueFormats.watt = kbn.formatBuilders.decimalSIPrefix('W');
+kbn.valueFormats.kwatt = kbn.formatBuilders.decimalSIPrefix('W', 1);
+kbn.valueFormats.voltamp = kbn.formatBuilders.decimalSIPrefix('VA');
+kbn.valueFormats.kvoltamp = kbn.formatBuilders.decimalSIPrefix('VA', 1);
+kbn.valueFormats.voltampreact = kbn.formatBuilders.decimalSIPrefix('var');
+kbn.valueFormats.kvoltampreact = kbn.formatBuilders.decimalSIPrefix('var', 1);
+kbn.valueFormats.watth = kbn.formatBuilders.decimalSIPrefix('Wh');
+kbn.valueFormats.kwatth = kbn.formatBuilders.decimalSIPrefix('Wh', 1);
+kbn.valueFormats.joule = kbn.formatBuilders.decimalSIPrefix('J');
+kbn.valueFormats.ev = kbn.formatBuilders.decimalSIPrefix('eV');
+kbn.valueFormats.amp = kbn.formatBuilders.decimalSIPrefix('A');
+kbn.valueFormats.kamp = kbn.formatBuilders.decimalSIPrefix('A', 1);
+kbn.valueFormats.volt = kbn.formatBuilders.decimalSIPrefix('V');
+kbn.valueFormats.kvolt = kbn.formatBuilders.decimalSIPrefix('V', 1);
+kbn.valueFormats.dBm = kbn.formatBuilders.decimalSIPrefix('dBm');
+
+// Temperature
+kbn.valueFormats.celsius = kbn.formatBuilders.fixedUnit('°C');
+kbn.valueFormats.farenheit = kbn.formatBuilders.fixedUnit('°F');
+kbn.valueFormats.kelvin = kbn.formatBuilders.fixedUnit('K');
+kbn.valueFormats.humidity = kbn.formatBuilders.fixedUnit('%H');
+
+// Pressure
+kbn.valueFormats.pressurebar = kbn.formatBuilders.decimalSIPrefix('bar');
+kbn.valueFormats.pressurembar = kbn.formatBuilders.decimalSIPrefix('bar', -1);
+kbn.valueFormats.pressurekbar = kbn.formatBuilders.decimalSIPrefix('bar', 1);
+kbn.valueFormats.pressurehpa = kbn.formatBuilders.fixedUnit('hPa');
+kbn.valueFormats.pressurehg = kbn.formatBuilders.fixedUnit('"Hg');
+kbn.valueFormats.pressurepsi = kbn.formatBuilders.scaledUnits(1000, [' psi', ' ksi', ' Mpsi']);
+
+// Force
+kbn.valueFormats.forceNm = kbn.formatBuilders.decimalSIPrefix('Nm');
+kbn.valueFormats.forcekNm = kbn.formatBuilders.decimalSIPrefix('Nm', 1);
+kbn.valueFormats.forceN = kbn.formatBuilders.decimalSIPrefix('N');
+kbn.valueFormats.forcekN = kbn.formatBuilders.decimalSIPrefix('N', 1);
+
+// Length
+kbn.valueFormats.lengthm = kbn.formatBuilders.decimalSIPrefix('m');
+kbn.valueFormats.lengthmm = kbn.formatBuilders.decimalSIPrefix('m', -1);
+kbn.valueFormats.lengthkm = kbn.formatBuilders.decimalSIPrefix('m', 1);
+kbn.valueFormats.lengthmi = kbn.formatBuilders.fixedUnit('mi');
+
+// Mass
+kbn.valueFormats.massmg = kbn.formatBuilders.decimalSIPrefix('g', -1);
+kbn.valueFormats.massg = kbn.formatBuilders.decimalSIPrefix('g');
+kbn.valueFormats.masskg = kbn.formatBuilders.decimalSIPrefix('g', 1);
+kbn.valueFormats.masst = kbn.formatBuilders.fixedUnit('t');
+
+// Velocity
+kbn.valueFormats.velocityms = kbn.formatBuilders.fixedUnit('m/s');
+kbn.valueFormats.velocitykmh = kbn.formatBuilders.fixedUnit('km/h');
+kbn.valueFormats.velocitymph = kbn.formatBuilders.fixedUnit('mph');
+kbn.valueFormats.velocityknot = kbn.formatBuilders.fixedUnit('kn');
+
+// Volume
+kbn.valueFormats.litre = kbn.formatBuilders.decimalSIPrefix('L');
+kbn.valueFormats.mlitre = kbn.formatBuilders.decimalSIPrefix('L', -1);
+kbn.valueFormats.m3 = kbn.formatBuilders.decimalSIPrefix('m3');
+kbn.valueFormats.dm3 = kbn.formatBuilders.decimalSIPrefix('dm3');
+kbn.valueFormats.gallons = kbn.formatBuilders.fixedUnit('gal');
+
+// Flow
+kbn.valueFormats.flowgpm = kbn.formatBuilders.fixedUnit('gpm');
+kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms');
+kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs');
+kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm');
+
+// Time
+kbn.valueFormats.hertz = kbn.formatBuilders.decimalSIPrefix('Hz');
+
+kbn.valueFormats.ms = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 1000) {
+    return kbn.toFixed(size, decimals) + ' ms';
+  } else if (Math.abs(size) < 60000) {
+    // Less than 1 min
+    return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' s');
+  } else if (Math.abs(size) < 3600000) {
+    // Less than 1 hour, devide in minutes
+    return kbn.toFixedScaled(size / 60000, decimals, scaledDecimals, 5, ' min');
+  } else if (Math.abs(size) < 86400000) {
+    // Less than one day, devide in hours
+    return kbn.toFixedScaled(size / 3600000, decimals, scaledDecimals, 7, ' hour');
+  } else if (Math.abs(size) < 31536000000) {
+    // Less than one year, devide in days
+    return kbn.toFixedScaled(size / 86400000, decimals, scaledDecimals, 8, ' day');
+  }
+
+  return kbn.toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year');
+};
+
+kbn.valueFormats.s = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  // Less than 1 µs, devide in ns
+  if (Math.abs(size) < 0.000001) {
+    return kbn.toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns');
+  }
+  // Less than 1 ms, devide in µs
+  if (Math.abs(size) < 0.001) {
+    return kbn.toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs');
+  }
+  // Less than 1 second, devide in ms
+  if (Math.abs(size) < 1) {
+    return kbn.toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms');
+  }
+
+  if (Math.abs(size) < 60) {
+    return kbn.toFixed(size, decimals) + ' s';
+  } else if (Math.abs(size) < 3600) {
+    // Less than 1 hour, devide in minutes
+    return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 1, ' min');
+  } else if (Math.abs(size) < 86400) {
+    // Less than one day, devide in hours
+    return kbn.toFixedScaled(size / 3600, decimals, scaledDecimals, 4, ' hour');
+  } else if (Math.abs(size) < 604800) {
+    // Less than one week, devide in days
+    return kbn.toFixedScaled(size / 86400, decimals, scaledDecimals, 5, ' day');
+  } else if (Math.abs(size) < 31536000) {
+    // Less than one year, devide in week
+    return kbn.toFixedScaled(size / 604800, decimals, scaledDecimals, 6, ' week');
+  }
+
+  return kbn.toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year');
+};
+
+kbn.valueFormats['µs'] = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 1000) {
+    return kbn.toFixed(size, decimals) + ' µs';
+  } else if (Math.abs(size) < 1000000) {
+    return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' ms');
+  } else {
+    return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' s');
+  }
+};
+
+kbn.valueFormats.ns = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 1000) {
+    return kbn.toFixed(size, decimals) + ' ns';
+  } else if (Math.abs(size) < 1000000) {
+    return kbn.toFixedScaled(size / 1000, decimals, scaledDecimals, 3, ' µs');
+  } else if (Math.abs(size) < 1000000000) {
+    return kbn.toFixedScaled(size / 1000000, decimals, scaledDecimals, 6, ' ms');
+  } else if (Math.abs(size) < 60000000000) {
+    return kbn.toFixedScaled(size / 1000000000, decimals, scaledDecimals, 9, ' s');
+  } else {
+    return kbn.toFixedScaled(size / 60000000000, decimals, scaledDecimals, 12, ' min');
+  }
+};
+
+kbn.valueFormats.m = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 60) {
+    return kbn.toFixed(size, decimals) + ' min';
+  } else if (Math.abs(size) < 1440) {
+    return kbn.toFixedScaled(size / 60, decimals, scaledDecimals, 2, ' hour');
+  } else if (Math.abs(size) < 10080) {
+    return kbn.toFixedScaled(size / 1440, decimals, scaledDecimals, 3, ' day');
+  } else if (Math.abs(size) < 604800) {
+    return kbn.toFixedScaled(size / 10080, decimals, scaledDecimals, 4, ' week');
+  } else {
+    return kbn.toFixedScaled(size / 5.25948e5, decimals, scaledDecimals, 5, ' year');
+  }
+};
+
+kbn.valueFormats.h = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 24) {
+    return kbn.toFixed(size, decimals) + ' hour';
+  } else if (Math.abs(size) < 168) {
+    return kbn.toFixedScaled(size / 24, decimals, scaledDecimals, 2, ' day');
+  } else if (Math.abs(size) < 8760) {
+    return kbn.toFixedScaled(size / 168, decimals, scaledDecimals, 3, ' week');
+  } else {
+    return kbn.toFixedScaled(size / 8760, decimals, scaledDecimals, 4, ' year');
+  }
+};
+
+kbn.valueFormats.d = function(size, decimals, scaledDecimals) {
+  if (size === null) {
+    return '';
+  }
+
+  if (Math.abs(size) < 7) {
+    return kbn.toFixed(size, decimals) + ' day';
+  } else if (Math.abs(size) < 365) {
+    return kbn.toFixedScaled(size / 7, decimals, scaledDecimals, 2, ' week');
+  } else {
+    return kbn.toFixedScaled(size / 365, decimals, scaledDecimals, 3, ' year');
+  }
+};
+
+kbn.toDuration = function(size, decimals, timeScale) {
+  if (size === null) {
+    return '';
+  }
+  if (size === 0) {
+    return '0 ' + timeScale + 's';
+  }
+  if (size < 0) {
+    return kbn.toDuration(-size, decimals, timeScale) + ' ago';
+  }
+
+  var units = [
+    { short: 'y', long: 'year' },
+    { short: 'M', long: 'month' },
+    { short: 'w', long: 'week' },
+    { short: 'd', long: 'day' },
+    { short: 'h', long: 'hour' },
+    { short: 'm', long: 'minute' },
+    { short: 's', long: 'second' },
+    { short: 'ms', long: 'millisecond' },
+  ];
+  // convert $size to milliseconds
+  // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors
+  size *=
+    kbn.intervals_in_seconds[
+      units.find(function(e) {
+        return e.long === timeScale;
+      }).short
+    ] * 1000;
+
+  var strings = [];
+  // after first value >= 1 print only $decimals more
+  var decrementDecimals = false;
+  for (var i = 0; i < units.length && decimals >= 0; i++) {
+    var interval = kbn.intervals_in_seconds[units[i].short] * 1000;
+    var value = size / interval;
+    if (value >= 1 || decrementDecimals) {
+      decrementDecimals = true;
+      var floor = Math.floor(value);
+      var unit = units[i].long + (floor !== 1 ? 's' : '');
+      strings.push(floor + ' ' + unit);
+      size = size % interval;
+      decimals--;
+    }
+  }
+
+  return strings.join(', ');
+};
+
+kbn.valueFormats.dtdurationms = function(size, decimals) {
+  return kbn.toDuration(size, decimals, 'millisecond');
+};
+
+kbn.valueFormats.dtdurations = function(size, decimals) {
+  return kbn.toDuration(size, decimals, 'second');
+};
+
+kbn.valueFormats.dateTimeAsIso = function(epoch) {
+  var time = moment(epoch);
+
+  if (moment().isSame(epoch, 'day')) {
+    return time.format('HH:mm:ss');
+  }
+  return time.format('YYYY-MM-DD HH:mm:ss');
+};
+
+kbn.valueFormats.dateTimeAsUS = function(epoch) {
+  var time = moment(epoch);
+
+  if (moment().isSame(epoch, 'day')) {
+    return time.format('h:mm:ss a');
+  }
+  return time.format('MM/DD/YYYY h:mm:ss a');
+};
+
+kbn.valueFormats.dateTimeFromNow = function(epoch) {
+  return moment(epoch).fromNow();
+};
+
+///// FORMAT MENU /////
+
+kbn.getUnitFormats = function() {
+  return [
+    {
+      text: 'none',
+      submenu: [
+        { text: 'none', value: 'none' },
+        { text: 'short', value: 'short' },
+        { text: 'percent (0-100)', value: 'percent' },
+        { text: 'percent (0.0-1.0)', value: 'percentunit' },
+        { text: 'Humidity (%H)', value: 'humidity' },
+        { text: 'ppm', value: 'ppm' },
+        { text: 'decibel', value: 'dB' },
+        { text: 'hexadecimal (0x)', value: 'hex0x' },
+        { text: 'hexadecimal', value: 'hex' },
+        { text: 'scientific notation', value: 'sci' },
+        { text: 'locale format', value: 'locale' },
+      ],
+    },
+    {
+      text: 'currency',
+      submenu: [
+        { text: 'Dollars ($)', value: 'currencyUSD' },
+        { text: 'Pounds (£)', value: 'currencyGBP' },
+        { text: 'Euro (€)', value: 'currencyEUR' },
+        { text: 'Yen (¥)', value: 'currencyJPY' },
+        { text: 'Rubles (₽)', value: 'currencyRUB' },
+        { text: 'Hryvnias (₴)', value: 'currencyUAH' },
+        { text: 'Real (R$)', value: 'currencyBRL' },
+        { text: 'Danish Krone (kr)', value: 'currencyDKK' },
+        { text: 'Icelandic Krone (kr)', value: 'currencyISK' },
+        { text: 'Norwegian Krone (kr)', value: 'currencyNOK' },
+        { text: 'Swedish Krone (kr)', value: 'currencySEK' },
+      ],
+    },
+    {
+      text: 'time',
+      submenu: [
+        { text: 'Hertz (1/s)', value: 'hertz' },
+        { text: 'nanoseconds (ns)', value: 'ns' },
+        { text: 'microseconds (µs)', value: 'µs' },
+        { text: 'milliseconds (ms)', value: 'ms' },
+        { text: 'seconds (s)', value: 's' },
+        { text: 'minutes (m)', value: 'm' },
+        { text: 'hours (h)', value: 'h' },
+        { text: 'days (d)', value: 'd' },
+        { text: 'duration (ms)', value: 'dtdurationms' },
+        { text: 'duration (s)', value: 'dtdurations' },
+      ],
+    },
+    {
+      text: 'date & time',
+      submenu: [
+        { text: 'YYYY-MM-DD HH:mm:ss', value: 'dateTimeAsIso' },
+        { text: 'DD/MM/YYYY h:mm:ss a', value: 'dateTimeAsUS' },
+        { text: 'From Now', value: 'dateTimeFromNow' },
+      ],
+    },
+    {
+      text: 'data (IEC)',
+      submenu: [
+        { text: 'bits', value: 'bits' },
+        { text: 'bytes', value: 'bytes' },
+        { text: 'kibibytes', value: 'kbytes' },
+        { text: 'mebibytes', value: 'mbytes' },
+        { text: 'gibibytes', value: 'gbytes' },
+      ],
+    },
+    {
+      text: 'data (Metric)',
+      submenu: [
+        { text: 'bits', value: 'decbits' },
+        { text: 'bytes', value: 'decbytes' },
+        { text: 'kilobytes', value: 'deckbytes' },
+        { text: 'megabytes', value: 'decmbytes' },
+        { text: 'gigabytes', value: 'decgbytes' },
+      ],
+    },
+    {
+      text: 'data rate',
+      submenu: [
+        { text: 'packets/sec', value: 'pps' },
+        { text: 'bits/sec', value: 'bps' },
+        { text: 'bytes/sec', value: 'Bps' },
+        { text: 'kilobits/sec', value: 'Kbits' },
+        { text: 'kilobytes/sec', value: 'KBs' },
+        { text: 'megabits/sec', value: 'Mbits' },
+        { text: 'megabytes/sec', value: 'MBs' },
+        { text: 'gigabytes/sec', value: 'GBs' },
+        { text: 'gigabits/sec', value: 'Gbits' },
+      ],
+    },
+    {
+      text: 'throughput',
+      submenu: [
+        { text: 'ops/sec (ops)', value: 'ops' },
+        { text: 'reads/sec (rps)', value: 'rps' },
+        { text: 'writes/sec (wps)', value: 'wps' },
+        { text: 'I/O ops/sec (iops)', value: 'iops' },
+        { text: 'ops/min (opm)', value: 'opm' },
+        { text: 'reads/min (rpm)', value: 'rpm' },
+        { text: 'writes/min (wpm)', value: 'wpm' },
+      ],
+    },
+    {
+      text: 'length',
+      submenu: [
+        { text: 'millimetre (mm)', value: 'lengthmm' },
+        { text: 'meter (m)', value: 'lengthm' },
+        { text: 'kilometer (km)', value: 'lengthkm' },
+        { text: 'mile (mi)', value: 'lengthmi' },
+      ],
+    },
+    {
+      text: 'mass',
+      submenu: [
+        { text: 'milligram (mg)', value: 'massmg' },
+        { text: 'gram (g)', value: 'massg' },
+        { text: 'kilogram (kg)', value: 'masskg' },
+        { text: 'metric ton (t)', value: 'masst' },
+      ],
+    },
+    {
+      text: 'velocity',
+      submenu: [
+        { text: 'm/s', value: 'velocityms' },
+        { text: 'km/h', value: 'velocitykmh' },
+        { text: 'mph', value: 'velocitymph' },
+        { text: 'knot (kn)', value: 'velocityknot' },
+      ],
+    },
+    {
+      text: 'volume',
+      submenu: [
+        { text: 'millilitre', value: 'mlitre' },
+        { text: 'litre', value: 'litre' },
+        { text: 'cubic metre', value: 'm3' },
+        { text: 'cubic decimetre', value: 'dm3' },
+        { text: 'gallons', value: 'gallons' },
+      ],
+    },
+    {
+      text: 'energy',
+      submenu: [
+        { text: 'watt (W)', value: 'watt' },
+        { text: 'kilowatt (kW)', value: 'kwatt' },
+        { text: 'volt-ampere (VA)', value: 'voltamp' },
+        { text: 'kilovolt-ampere (kVA)', value: 'kvoltamp' },
+        { text: 'volt-ampere reactive (var)', value: 'voltampreact' },
+        { text: 'kilovolt-ampere reactive (kvar)', value: 'kvoltampreact' },
+        { text: 'watt-hour (Wh)', value: 'watth' },
+        { text: 'kilowatt-hour (kWh)', value: 'kwatth' },
+        { text: 'joule (J)', value: 'joule' },
+        { text: 'electron volt (eV)', value: 'ev' },
+        { text: 'Ampere (A)', value: 'amp' },
+        { text: 'Kiloampere (kA)', value: 'kamp' },
+        { text: 'Volt (V)', value: 'volt' },
+        { text: 'Kilovolt (kV)', value: 'kvolt' },
+        { text: 'Decibel-milliwatt (dBm)', value: 'dBm' },
+      ],
+    },
+    {
+      text: 'temperature',
+      submenu: [
+        { text: 'Celsius (°C)', value: 'celsius' },
+        { text: 'Farenheit (°F)', value: 'farenheit' },
+        { text: 'Kelvin (K)', value: 'kelvin' },
+      ],
+    },
+    {
+      text: 'pressure',
+      submenu: [
+        { text: 'Millibars', value: 'pressurembar' },
+        { text: 'Bars', value: 'pressurebar' },
+        { text: 'Kilobars', value: 'pressurekbar' },
+        { text: 'Hectopascals', value: 'pressurehpa' },
+        { text: 'Inches of mercury', value: 'pressurehg' },
+        { text: 'PSI', value: 'pressurepsi' },
+      ],
+    },
+    {
+      text: 'force',
+      submenu: [
+        { text: 'Newton-meters (Nm)', value: 'forceNm' },
+        { text: 'Kilonewton-meters (kNm)', value: 'forcekNm' },
+        { text: 'Newtons (N)', value: 'forceN' },
+        { text: 'Kilonewtons (kN)', value: 'forcekN' },
+      ],
+    },
+    {
+      text: 'flow',
+      submenu: [
+        { text: 'Gallons/min (gpm)', value: 'flowgpm' },
+        { text: 'Cubic meters/sec (cms)', value: 'flowcms' },
+        { text: 'Cubic feet/sec (cfs)', value: 'flowcfs' },
+        { text: 'Cubic feet/min (cfm)', value: 'flowcfm' },
+      ],
+    },
+  ];
+};
+
+export default kbn;

+ 2 - 0
public/app/features/dashboard/dashboardLoaderSrv.js

@@ -11,6 +11,8 @@ define([
 function (angular, moment, _, $, kbn, dateMath, impressionStore) {
   'use strict';
 
+  kbn = kbn.default;
+
   var module = angular.module('grafana.services');
 
   module.service('dashboardLoaderSrv', function(backendSrv,

+ 5 - 7
public/app/features/dashboard/export/export_modal.ts

@@ -1,8 +1,7 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import angular from 'angular';
-import coreModule from 'app/core/core_module';
+import {saveAs} from 'file-saver';
 
+import coreModule from 'app/core/core_module';
 import {DashboardExporter} from './exporter';
 
 export class DashExportCtrl {
@@ -22,9 +21,8 @@ export class DashExportCtrl {
   }
 
   save() {
-    var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" });
-    var wnd: any = window;
-    wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
+    var blob = new Blob([angular.toJson(this.dash, true)], {type: 'application/json;charset=utf-8'});
+    saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
   }
 
   saveJson() {
@@ -44,7 +42,7 @@ export function dashExportDirective() {
     controller: DashExportCtrl,
     bindToController: true,
     controllerAs: 'ctrl',
-    scope: {dismiss: "&"}
+    scope: {dismiss: '&'},
   };
 }
 

+ 2 - 0
public/app/features/panellinks/linkSrv.js

@@ -6,6 +6,8 @@ define([
 function (angular, _, kbn) {
   'use strict';
 
+  kbn = kbn.default;
+
   angular
     .module('grafana.services')
     .service('linkSrv', function(templateSrv, timeSrv) {

+ 0 - 0
public/app/features/plugins/buit_in_plugins.ts → public/app/features/plugins/built_in_plugins.ts


+ 21 - 4
public/app/features/plugins/plugin_loader.ts

@@ -9,15 +9,24 @@ import config from 'app/core/config';
 import TimeSeries from 'app/core/time_series2';
 import TableModel from 'app/core/table_model';
 import {coreModule, appEvents, contextSrv} from 'app/core/core';
-import {Observable} from 'rxjs/Observable';
-import {Subject} from 'rxjs/Subject';
 import * as datemath from 'app/core/utils/datemath';
 import * as fileExport from 'app/core/utils/file_export';
 import * as flatten from 'app/core/utils/flatten';
 import * as ticks from 'app/core/utils/ticks';
-import builtInPlugins from './buit_in_plugins';
+import {impressions} from 'app/features/dashboard/impression_store';
+import builtInPlugins from './built_in_plugins';
 import d3 from 'vendor/d3/d3';
 
+// rxjs
+import {Observable} from 'rxjs/Observable';
+import {Subject} from 'rxjs/Subject';
+
+// these imports add functions to Observable
+import 'rxjs/add/observable/empty';
+import 'rxjs/add/observable/from';
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/combineAll';
+
 System.config({
   baseURL: 'public',
   defaultExtension: 'js',
@@ -30,6 +39,9 @@ System.config({
     text: 'vendor/plugin-text/text.js',
     css: 'vendor/plugin-css/css.js'
   },
+  meta: {
+    '*': {esModule: true}
+  }
 });
 
 // add cache busting
@@ -56,8 +68,13 @@ exposeToPlugin('rxjs/Subject', Subject);
 exposeToPlugin('rxjs/Observable', Observable);
 exposeToPlugin('d3', d3);
 
-exposeToPlugin('app/plugins/sdk', sdk);
+exposeToPlugin('app/features/dashboard/impression_store', {
+  impressions: impressions,
+  __esModule: true
+});
+
 
+exposeToPlugin('app/plugins/sdk', sdk);
 exposeToPlugin('app/core/utils/datemath', datemath);
 exposeToPlugin('app/core/utils/file_export', fileExport);
 exposeToPlugin('app/core/utils/flatten', flatten);

+ 0 - 2
public/app/features/templating/adhoc_variable.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import {Variable, assignModelProperties, variableTypes} from './variable';
 

+ 12 - 8
public/app/features/templating/all.ts

@@ -1,15 +1,19 @@
-import './templateSrv';
 import './editor_ctrl';
+import coreModule from 'app/core/core_module';
 
-import {VariableSrv} from './variable_srv';
-import {IntervalVariable} from './interval_variable';
-import {QueryVariable} from './query_variable';
-import {DatasourceVariable} from './datasource_variable';
-import {CustomVariable} from './custom_variable';
-import {ConstantVariable} from './constant_variable';
-import {AdhocVariable} from './adhoc_variable';
+import { TemplateSrv } from './template_srv';
+import { VariableSrv } from './variable_srv';
+import { IntervalVariable } from './interval_variable';
+import { QueryVariable } from './query_variable';
+import { DatasourceVariable } from './datasource_variable';
+import { CustomVariable } from './custom_variable';
+import { ConstantVariable } from './constant_variable';
+import { AdhocVariable } from './adhoc_variable';
+
+coreModule.service('templateSrv', TemplateSrv);
 
 export {
+  TemplateSrv,
   VariableSrv,
   IntervalVariable,
   QueryVariable,

+ 0 - 2
public/app/features/templating/query_variable.ts

@@ -1,5 +1,3 @@
-///<reference path="../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import kbn from 'app/core/utils/kbn';
 import {Variable, containsVariable, assignModelProperties, variableTypes} from './variable';

+ 10 - 12
public/app/features/templating/specs/adhoc_variable_specs.ts → public/app/features/templating/specs/adhoc_variable.jest.ts

@@ -1,5 +1,3 @@
-import {describe, it, expect} from 'test/lib/common';
-
 import {AdhocVariable} from '../adhoc_variable';
 
 describe('AdhocVariable', function() {
@@ -15,7 +13,7 @@ describe('AdhocVariable', function() {
         ]
       });
       var urlValue = variable.getValueForUrl();
-      expect(urlValue).to.eql(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfp__value3b__gfp__value3c"]);
+      expect(urlValue).toMatchObject(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfp__value3b__gfp__value3c"]);
     });
 
   });
@@ -26,17 +24,17 @@ describe('AdhocVariable', function() {
       var variable = new AdhocVariable({});
       variable.setValueFromUrl(["key1|=|value1", "key2|!=|value2", "key3|=|value3a__gfp__value3b__gfp__value3c"]);
 
-      expect(variable.filters[0].key).to.be('key1');
-      expect(variable.filters[0].operator).to.be('=');
-      expect(variable.filters[0].value).to.be('value1');
+      expect(variable.filters[0].key).toBe('key1');
+      expect(variable.filters[0].operator).toBe('=');
+      expect(variable.filters[0].value).toBe('value1');
 
-      expect(variable.filters[1].key).to.be('key2');
-      expect(variable.filters[1].operator).to.be('!=');
-      expect(variable.filters[1].value).to.be('value2');
+      expect(variable.filters[1].key).toBe('key2');
+      expect(variable.filters[1].operator).toBe('!=');
+      expect(variable.filters[1].value).toBe('value2');
 
-      expect(variable.filters[2].key).to.be('key3');
-      expect(variable.filters[2].operator).to.be('=');
-      expect(variable.filters[2].value).to.be('value3a|value3b|value3c');
+      expect(variable.filters[2].key).toBe('key3');
+      expect(variable.filters[2].operator).toBe('=');
+      expect(variable.filters[2].value).toBe('value3a|value3b|value3c');
     });
 
   });

+ 22 - 24
public/app/features/templating/specs/query_variable_specs.ts → public/app/features/templating/specs/query_variable.jest.ts

@@ -1,5 +1,3 @@
-import {describe, it, expect} from 'test/lib/common';
-
 import {QueryVariable} from '../query_variable';
 
 describe('QueryVariable', () => {
@@ -8,14 +6,14 @@ describe('QueryVariable', () => {
 
     it('should set defaults', () => {
       var variable = new QueryVariable({}, null, null, null, null);
-      expect(variable.datasource).to.be(null);
-      expect(variable.refresh).to.be(0);
-      expect(variable.sort).to.be(0);
-      expect(variable.name).to.be('');
-      expect(variable.hide).to.be(0);
-      expect(variable.options.length).to.be(0);
-      expect(variable.multi).to.be(false);
-      expect(variable.includeAll).to.be(false);
+      expect(variable.datasource).toBe(null);
+      expect(variable.refresh).toBe(0);
+      expect(variable.sort).toBe(0);
+      expect(variable.name).toBe('');
+      expect(variable.hide).toBe(0);
+      expect(variable.options.length).toBe(0);
+      expect(variable.multi).toBe(false);
+      expect(variable.includeAll).toBe(false);
     });
 
     it('get model should copy changes back to model', () => {
@@ -26,11 +24,11 @@ describe('QueryVariable', () => {
       variable.sort = 50;
 
       var model = variable.getSaveModel();
-      expect(model.options.length).to.be(1);
-      expect(model.options[0].text).to.be('test');
-      expect(model.datasource).to.be('google');
-      expect(model.regex).to.be('asd');
-      expect(model.sort).to.be(50);
+      expect(model.options.length).toBe(1);
+      expect(model.options[0].text).toBe('test');
+      expect(model.datasource).toBe('google');
+      expect(model.regex).toBe('asd');
+      expect(model.sort).toBe(50);
     });
 
     it('if refresh != 0 then remove options in presisted mode', () => {
@@ -39,7 +37,7 @@ describe('QueryVariable', () => {
       variable.refresh = 1;
 
       var model = variable.getSaveModel();
-      expect(model.options.length).to.be(0);
+      expect(model.options.length).toBe(0);
     });
   });
 
@@ -67,14 +65,14 @@ describe('QueryVariable', () => {
       it('should return in same order', () => {
         var i = 0;
 
-        expect(result.length).to.be(11);
-        expect(result[i++].text).to.be('');
-        expect(result[i++].text).to.be('0');
-        expect(result[i++].text).to.be('1');
-        expect(result[i++].text).to.be('3');
-        expect(result[i++].text).to.be('4');
-        expect(result[i++].text).to.be('5');
-        expect(result[i++].text).to.be('6');
+        expect(result.length).toBe(11);
+        expect(result[i++].text).toBe('');
+        expect(result[i++].text).toBe('0');
+        expect(result[i++].text).toBe('1');
+        expect(result[i++].text).toBe('3');
+        expect(result[i++].text).toBe('4');
+        expect(result[i++].text).toBe('5');
+        expect(result[i++].text).toBe('6');
       });
     });
   });

+ 44 - 52
public/app/features/templating/specs/template_srv_specs.ts → public/app/features/templating/specs/template_srv.jest.ts

@@ -1,28 +1,11 @@
-import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
-
-import '../all';
-import {Emitter} from 'app/core/core';
+import { TemplateSrv } from '../template_srv';
 
 describe('templateSrv', function() {
-  var _templateSrv, _variableSrv;
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module($provide => {
-    $provide.value('timeSrv', {});
-    $provide.value('datasourceSrv', {});
-  }));
-
-  beforeEach(angularMocks.inject(function(variableSrv, templateSrv) {
-    _templateSrv = templateSrv;
-    _variableSrv = variableSrv;
-  }));
+  var _templateSrv;
 
   function initTemplateSrv(variables) {
-    _variableSrv.init({
-      templating: {list: variables},
-      events: new Emitter(),
-    });
+    _templateSrv = new TemplateSrv();
+    _templateSrv.init(variables);
   }
 
   describe('init', function() {
@@ -32,7 +15,7 @@ describe('templateSrv', function() {
 
     it('should initialize template data', function() {
       var target = _templateSrv.replace('this.[[test]].filters');
-      expect(target).to.be('this.oogle.filters');
+      expect(target).toBe('this.oogle.filters');
     });
   });
 
@@ -43,12 +26,12 @@ describe('templateSrv', function() {
 
     it('should replace $test with scoped value', function() {
       var target = _templateSrv.replace('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
-      expect(target).to.be('this.mupp.filters');
+      expect(target).toBe('this.mupp.filters');
     });
 
     it('should replace $test with scoped text', function() {
       var target = _templateSrv.replaceWithText('this.$test.filters', {'test': {value: 'mupp', text: 'asd'}});
-      expect(target).to.be('this.asd.filters');
+      expect(target).toBe('this.asd.filters');
     });
   });
 
@@ -63,17 +46,17 @@ describe('templateSrv', function() {
 
     it('should return filters if datasourceName match', function() {
       var filters = _templateSrv.getAdhocFilters('oogle');
-      expect(filters).to.eql([1]);
+      expect(filters).toMatchObject([1]);
     });
 
     it('should return empty array if datasourceName does not match', function() {
       var filters = _templateSrv.getAdhocFilters('oogleasdasd');
-      expect(filters).to.eql([]);
+      expect(filters).toMatchObject([]);
     });
 
     it('should return filters when datasourceName match via data source variable', function() {
       var filters = _templateSrv.getAdhocFilters('logstash');
-      expect(filters).to.eql([2]);
+      expect(filters).toMatchObject([2]);
     });
   });
 
@@ -84,17 +67,17 @@ describe('templateSrv', function() {
 
     it('should replace $test with globbed value', function() {
       var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
-      expect(target).to.be('this.{value1,value2}.filters');
+      expect(target).toBe('this.{value1,value2}.filters');
     });
 
     it('should replace $test with piped value', function() {
       var target = _templateSrv.replace('this=$test', {}, 'pipe');
-      expect(target).to.be('this=value1|value2');
+      expect(target).toBe('this=value1|value2');
     });
 
     it('should replace $test with piped value', function() {
       var target = _templateSrv.replace('this=$test', {}, 'pipe');
-      expect(target).to.be('this=value1|value2');
+      expect(target).toBe('this=value1|value2');
     });
   });
 
@@ -112,7 +95,7 @@ describe('templateSrv', function() {
 
     it('should replace $test with formatted all value', function() {
       var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
-      expect(target).to.be('this.{value1,value2}.filters');
+      expect(target).toBe('this.{value1,value2}.filters');
     });
   });
 
@@ -131,12 +114,12 @@ describe('templateSrv', function() {
 
     it('should replace $test with formatted all value', function() {
       var target = _templateSrv.replace('this.$test.filters', {}, 'glob');
-      expect(target).to.be('this.*.filters');
+      expect(target).toBe('this.*.filters');
     });
 
     it('should not escape custom all value', function() {
       var target = _templateSrv.replace('this.$test', {}, 'regex');
-      expect(target).to.be('this.*');
+      expect(target).toBe('this.*');
     });
   });
 
@@ -144,49 +127,49 @@ describe('templateSrv', function() {
     it('should properly escape $test with lucene escape sequences', function() {
       initTemplateSrv([{type: 'query', name: 'test', current: {value: 'value/4' }}]);
       var target = _templateSrv.replace('this:$test', {}, 'lucene');
-      expect(target).to.be("this:value\\\/4");
+      expect(target).toBe("this:value\\\/4");
     });
   });
 
   describe('format variable to string values', function() {
     it('single value should return value', function() {
       var result = _templateSrv.formatValue('test');
-      expect(result).to.be('test');
+      expect(result).toBe('test');
     });
 
     it('multi value and glob format should render glob string', function() {
       var result = _templateSrv.formatValue(['test','test2'], 'glob');
-      expect(result).to.be('{test,test2}');
+      expect(result).toBe('{test,test2}');
     });
 
     it('multi value and lucene should render as lucene expr', function() {
       var result = _templateSrv.formatValue(['test','test2'], 'lucene');
-      expect(result).to.be('("test" OR "test2")');
+      expect(result).toBe('("test" OR "test2")');
     });
 
     it('multi value and regex format should render regex string', function() {
       var result = _templateSrv.formatValue(['test.','test2'], 'regex');
-      expect(result).to.be('(test\\.|test2)');
+      expect(result).toBe('(test\\.|test2)');
     });
 
     it('multi value and pipe should render pipe string', function() {
       var result = _templateSrv.formatValue(['test','test2'], 'pipe');
-      expect(result).to.be('test|test2');
+      expect(result).toBe('test|test2');
     });
 
     it('multi value and distributed should render distributed string', function() {
       var result = _templateSrv.formatValue(['test','test2'], 'distributed', { name: 'build' });
-      expect(result).to.be('test,build=test2');
+      expect(result).toBe('test,build=test2');
     });
 
     it('multi value and distributed should render when not string', function() {
       var result = _templateSrv.formatValue(['test'], 'distributed', { name: 'build' });
-      expect(result).to.be('test');
+      expect(result).toBe('test');
     });
 
     it('slash should be properly escaped in regex format', function() {
       var result = _templateSrv.formatValue('Gi3/14', 'regex');
-      expect(result).to.be('Gi3\\/14');
+      expect(result).toBe('Gi3\\/14');
     });
 
   });
@@ -198,7 +181,7 @@ describe('templateSrv', function() {
 
     it('should return true if exists', function() {
       var result = _templateSrv.variableExists('$test');
-      expect(result).to.be(true);
+      expect(result).toBe(true);
     });
   });
 
@@ -209,17 +192,17 @@ describe('templateSrv', function() {
 
     it('should insert html', function() {
       var result = _templateSrv.highlightVariablesAsHtml('$test');
-      expect(result).to.be('<span class="template-variable">$test</span>');
+      expect(result).toBe('<span class="template-variable">$test</span>');
     });
 
     it('should insert html anywhere in string', function() {
       var result = _templateSrv.highlightVariablesAsHtml('this $test ok');
-      expect(result).to.be('this <span class="template-variable">$test</span> ok');
+      expect(result).toBe('this <span class="template-variable">$test</span> ok');
     });
 
     it('should ignore if variables does not exist', function() {
       var result = _templateSrv.highlightVariablesAsHtml('this $google ok');
-      expect(result).to.be('this $google ok');
+      expect(result).toBe('this $google ok');
     });
   });
 
@@ -230,19 +213,28 @@ describe('templateSrv', function() {
 
     it('should set current value and update template data', function() {
       var target = _templateSrv.replace('this.[[test]].filters');
-      expect(target).to.be('this.muuuu.filters');
+      expect(target).toBe('this.muuuu.filters');
     });
   });
 
   describe('fillVariableValuesForUrl with multi value', function() {
     beforeEach(function() {
-      initTemplateSrv([{type: 'query', name: 'test', current: { value: ['val1', 'val2'] }}]);
+      initTemplateSrv([
+        {
+          type: 'query',
+          name: 'test',
+          current: { value: ['val1', 'val2'] },
+          getValueForUrl: function() {
+            return this.current.value;
+          }
+        }
+      ]);
     });
 
     it('should set multiple url params', function() {
       var params = {};
       _templateSrv.fillVariableValuesForUrl(params);
-      expect(params['var-test']).to.eql(['val1', 'val2']);
+      expect(params['var-test']).toMatchObject(['val1', 'val2']);
     });
   });
 
@@ -254,7 +246,7 @@ describe('templateSrv', function() {
     it('should set scoped value as url params', function() {
       var params = {};
       _templateSrv.fillVariableValuesForUrl(params, {'test': {value: 'val1'}});
-      expect(params['var-test']).to.eql('val1');
+      expect(params['var-test']).toBe('val1');
     });
   });
 
@@ -270,7 +262,7 @@ describe('templateSrv', function() {
 
     it('should replace with text except for grafanaVariables', function() {
       var target = _templateSrv.replaceWithText('Server: $server, period: $period');
-      expect(target).to.be('Server: All, period: 13m');
+      expect(target).toBe('Server: All, period: 13m');
     });
   });
 
@@ -281,7 +273,7 @@ describe('templateSrv', function() {
 
     it('should replace $__interval_ms with interval milliseconds', function() {
       var target = _templateSrv.replace('10 * $__interval_ms', {"__interval_ms": {text: "100", value: "100"}});
-      expect(target).to.be('10 * 100');
+      expect(target).toBe('10 * 100');
     });
 
   });

+ 0 - 253
public/app/features/templating/templateSrv.js

@@ -1,253 +0,0 @@
-define([
-  'angular',
-  'lodash',
-  'app/core/utils/kbn',
-],
-function (angular, _, kbn) {
-  'use strict';
-
-  var module = angular.module('grafana.services');
-
-  module.service('templateSrv', function() {
-    var self = this;
-
-    this._regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
-    this._index = {};
-    this._texts = {};
-    this._grafanaVariables = {};
-
-    // default built ins
-    this._builtIns = {};
-    this._builtIns['__interval'] = {text: '1s', value: '1s'};
-    this._builtIns['__interval_ms'] = {text: '100', value: '100'};
-
-    this.init = function(variables) {
-      this.variables = variables;
-      this.updateTemplateData();
-    };
-
-    this.updateTemplateData = function() {
-      this._index = {};
-      this._filters = {};
-
-      for (var i = 0; i < this.variables.length; i++) {
-        var variable = this.variables[i];
-
-        if (!variable.current || !variable.current.isNone && !variable.current.value) {
-          continue;
-        }
-
-        this._index[variable.name] = variable;
-      }
-    };
-
-    this.variableInitialized = function(variable) {
-      this._index[variable.name] = variable;
-    };
-
-    this.getAdhocFilters = function(datasourceName) {
-      var filters = [];
-
-      for (var i = 0; i < this.variables.length; i++) {
-        var variable = this.variables[i];
-        if (variable.type !== 'adhoc') {
-          continue;
-        }
-
-        if (variable.datasource === datasourceName) {
-          filters = filters.concat(variable.filters);
-        }
-
-        if (variable.datasource.indexOf('$') === 0) {
-          if (this.replace(variable.datasource) === datasourceName) {
-            filters = filters.concat(variable.filters);
-          }
-        }
-      }
-
-      return filters;
-    };
-
-    function luceneEscape(value) {
-      return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
-    }
-
-    this.luceneFormat = function(value) {
-      if (typeof value === 'string') {
-        return luceneEscape(value);
-      }
-      var quotedValues = _.map(value, function(val) {
-        return '\"' + luceneEscape(val) + '\"';
-      });
-      return '(' + quotedValues.join(' OR ') + ')';
-    };
-
-    this.formatValue = function(value, format, variable) {
-      // for some scopedVars there is no variable
-      variable = variable || {};
-
-      if (typeof format === 'function') {
-        return format(value, variable, this.formatValue);
-      }
-
-      switch(format) {
-        case "regex": {
-          if (typeof value === 'string') {
-            return kbn.regexEscape(value);
-          }
-
-          var escapedValues = _.map(value, kbn.regexEscape);
-          return '(' + escapedValues.join('|') + ')';
-        }
-        case "lucene": {
-          return this.luceneFormat(value, format, variable);
-        }
-        case "pipe": {
-          if (typeof value === 'string') {
-            return value;
-          }
-          return value.join('|');
-        }
-        case "distributed": {
-          if (typeof value === 'string') {
-            return value;
-          }
-          return this.distributeVariable(value, variable.name);
-        }
-        default:  {
-          if (_.isArray(value)) {
-            return '{' + value.join(',') + '}';
-          }
-          return value;
-        }
-      }
-    };
-
-    this.setGrafanaVariable = function (name, value) {
-      this._grafanaVariables[name] = value;
-    };
-
-    this.getVariableName = function(expression) {
-      this._regex.lastIndex = 0;
-      var match = this._regex.exec(expression);
-      if (!match) {
-        return null;
-      }
-      return match[1] || match[2];
-    };
-
-    this.variableExists = function(expression) {
-      var name = this.getVariableName(expression);
-      return name && (self._index[name] !== void 0);
-    };
-
-    this.highlightVariablesAsHtml = function(str) {
-      if (!str || !_.isString(str)) { return str; }
-
-      str = _.escape(str);
-      this._regex.lastIndex = 0;
-      return str.replace(this._regex, function(match, g1, g2) {
-        if (self._index[g1 || g2] || self._builtIns[g1 || g2]) {
-          return '<span class="template-variable">' + match + '</span>';
-        }
-        return match;
-      });
-    };
-
-    this.getAllValue = function(variable) {
-      if (variable.allValue) {
-        return variable.allValue;
-      }
-      var values = [];
-      for (var i = 1; i < variable.options.length; i++) {
-        values.push(variable.options[i].value);
-      }
-      return values;
-    };
-
-    this.replace = function(target, scopedVars, format) {
-      if (!target) { return target; }
-
-      var variable, systemValue, value;
-      this._regex.lastIndex = 0;
-
-      return target.replace(this._regex, function(match, g1, g2) {
-        variable = self._index[g1 || g2];
-
-        if (scopedVars) {
-          value = scopedVars[g1 || g2];
-          if (value) {
-            return self.formatValue(value.value, format, variable);
-          }
-        }
-
-        if (!variable) {
-          return match;
-        }
-
-        systemValue = self._grafanaVariables[variable.current.value];
-        if (systemValue) {
-          return self.formatValue(systemValue, format, variable);
-        }
-
-        value = variable.current.value;
-        if (self.isAllValue(value)) {
-          value = self.getAllValue(variable);
-          // skip formating of custom all values
-          if (variable.allValue) {
-            return value;
-          }
-        }
-
-        var res = self.formatValue(value, format, variable);
-        return res;
-      });
-    };
-
-    this.isAllValue = function(value) {
-      return value === '$__all' || Array.isArray(value) && value[0] === '$__all';
-    };
-
-    this.replaceWithText = function(target, scopedVars) {
-      if (!target) { return target; }
-
-      var variable;
-      this._regex.lastIndex = 0;
-
-      return target.replace(this._regex, function(match, g1, g2) {
-        if (scopedVars) {
-          var option = scopedVars[g1 || g2];
-          if (option) { return option.text; }
-        }
-
-        variable = self._index[g1 || g2];
-        if (!variable) { return match; }
-
-        return self._grafanaVariables[variable.current.value] || variable.current.text;
-      });
-    };
-
-    this.fillVariableValuesForUrl = function(params, scopedVars) {
-      _.each(this.variables, function(variable) {
-        if (scopedVars && scopedVars[variable.name] !== void 0) {
-          params['var-' + variable.name] = scopedVars[variable.name].value;
-        } else {
-          params['var-' + variable.name] = variable.getValueForUrl();
-        }
-      });
-    };
-
-    this.distributeVariable = function(value, variable) {
-      value = _.map(value, function(val, index) {
-        if (index !== 0) {
-          return variable + "=" + val;
-        } else {
-          return val;
-        }
-      });
-      return value.join(',');
-    };
-
-  });
-
-});

+ 244 - 0
public/app/features/templating/template_srv.ts

@@ -0,0 +1,244 @@
+import kbn from 'app/core/utils/kbn';
+import _ from 'lodash';
+
+function luceneEscape(value) {
+  return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, "\\$1");
+}
+
+export class TemplateSrv {
+  variables: any[];
+
+  private regex = /\$(\w+)|\[\[([\s\S]+?)\]\]/g;
+  private index = {};
+  private grafanaVariables = {};
+  private builtIns = {};
+  private filters = {};
+
+  constructor() {
+    this.builtIns['__interval'] = {text: '1s', value: '1s'};
+    this.builtIns['__interval_ms'] = {text: '100', value: '100'};
+  }
+
+  init(variables) {
+    this.variables = variables;
+    this.updateTemplateData();
+  }
+
+  updateTemplateData() {
+    this.index = {};
+    this.filters = {};
+
+    for (var i = 0; i < this.variables.length; i++) {
+      var variable = this.variables[i];
+
+      if (!variable.current || !variable.current.isNone && !variable.current.value) {
+        continue;
+      }
+
+      this.index[variable.name] = variable;
+    }
+  }
+
+  variableInitialized(variable) {
+    this.index[variable.name] = variable;
+  }
+
+  getAdhocFilters(datasourceName) {
+    var filters = [];
+
+    for (var i = 0; i < this.variables.length; i++) {
+      var variable = this.variables[i];
+      if (variable.type !== 'adhoc') {
+        continue;
+      }
+
+      if (variable.datasource === datasourceName) {
+        filters = filters.concat(variable.filters);
+      }
+
+      if (variable.datasource.indexOf('$') === 0) {
+        if (this.replace(variable.datasource) === datasourceName) {
+          filters = filters.concat(variable.filters);
+        }
+      }
+    }
+
+    return filters;
+  }
+
+  luceneFormat(value) {
+    if (typeof value === 'string') {
+      return luceneEscape(value);
+    }
+    var quotedValues = _.map(value, function(val) {
+      return '\"' + luceneEscape(val) + '\"';
+    });
+    return '(' + quotedValues.join(' OR ') + ')';
+  }
+
+  formatValue(value, format, variable) {
+    // for some scopedVars there is no variable
+    variable = variable || {};
+
+    if (typeof format === 'function') {
+      return format(value, variable, this.formatValue);
+    }
+
+    switch (format) {
+      case "regex": {
+        if (typeof value === 'string') {
+          return kbn.regexEscape(value);
+        }
+
+        var escapedValues = _.map(value, kbn.regexEscape);
+        return '(' + escapedValues.join('|') + ')';
+      }
+      case "lucene": {
+        return this.luceneFormat(value);
+      }
+      case "pipe": {
+        if (typeof value === 'string') {
+          return value;
+        }
+        return value.join('|');
+      }
+      case "distributed": {
+        if (typeof value === 'string') {
+          return value;
+        }
+        return this.distributeVariable(value, variable.name);
+      }
+      default:  {
+        if (_.isArray(value)) {
+          return '{' + value.join(',') + '}';
+        }
+        return value;
+      }
+    }
+  }
+
+  setGrafanaVariable(name, value) {
+    this.grafanaVariables[name] = value;
+  }
+
+  getVariableName(expression) {
+    this.regex.lastIndex = 0;
+    var match = this.regex.exec(expression);
+    if (!match) {
+      return null;
+    }
+    return match[1] || match[2];
+  }
+
+  variableExists(expression) {
+    var name = this.getVariableName(expression);
+    return name && (this.index[name] !== void 0);
+  }
+
+  highlightVariablesAsHtml(str) {
+    if (!str || !_.isString(str)) { return str; }
+
+    str = _.escape(str);
+    this.regex.lastIndex = 0;
+    return str.replace(this.regex, (match, g1, g2) => {
+      if (this.index[g1 || g2] || this.builtIns[g1 || g2]) {
+        return '<span class="template-variable">' + match + '</span>';
+      }
+      return match;
+    });
+  }
+
+  getAllValue(variable) {
+    if (variable.allValue) {
+      return variable.allValue;
+    }
+    var values = [];
+    for (var i = 1; i < variable.options.length; i++) {
+      values.push(variable.options[i].value);
+    }
+    return values;
+  }
+
+  replace(target, scopedVars?, format?) {
+    if (!target) { return target; }
+
+    var variable, systemValue, value;
+    this.regex.lastIndex = 0;
+
+    return target.replace(this.regex, (match, g1, g2) => {
+      variable = this.index[g1 || g2];
+
+      if (scopedVars) {
+        value = scopedVars[g1 || g2];
+        if (value) {
+          return this.formatValue(value.value, format, variable);
+        }
+      }
+
+      if (!variable) {
+        return match;
+      }
+
+      systemValue = this.grafanaVariables[variable.current.value];
+      if (systemValue) {
+        return this.formatValue(systemValue, format, variable);
+      }
+
+      value = variable.current.value;
+      if (this.isAllValue(value)) {
+        value = this.getAllValue(variable);
+        // skip formating of custom all values
+        if (variable.allValue) {
+          return value;
+        }
+      }
+
+      var res = this.formatValue(value, format, variable);
+      return res;
+    });
+  }
+
+  isAllValue(value) {
+    return value === '$__all' || Array.isArray(value) && value[0] === '$__all';
+  }
+
+  replaceWithText(target, scopedVars) {
+    if (!target) { return target; }
+
+    var variable;
+    this.regex.lastIndex = 0;
+
+    return target.replace(this.regex, (match, g1, g2) => {
+      if (scopedVars) {
+        var option = scopedVars[g1 || g2];
+        if (option) { return option.text; }
+      }
+
+      variable = this.index[g1 || g2];
+      if (!variable) { return match; }
+
+      return this.grafanaVariables[variable.current.value] || variable.current.text;
+    });
+  }
+
+  fillVariableValuesForUrl(params, scopedVars) {
+    _.each(this.variables, function(variable) {
+      if (scopedVars && scopedVars[variable.name] !== void 0) {
+        params['var-' + variable.name] = scopedVars[variable.name].value;
+      } else {
+        params['var-' + variable.name] = variable.getValueForUrl();
+      }
+    });
+  }
+
+  distributeVariable(value, variable) {
+    value = _.map(value, function(val, index) {
+      if (index !== 0) {
+        return variable + "=" + val;
+      } else {
+        return val;
+      }
+    });
+    return value.join(',');
+  }
+}

+ 1 - 3
public/app/features/templating/variable.ts

@@ -1,7 +1,5 @@
-///<reference path="../../headers/common.d.ts" />
-
 import kbn from 'app/core/utils/kbn';
-import {assignModelProperties} from 'app/core/core';
+import {assignModelProperties} from 'app/core/utils/model_utils';
 
 export interface Variable {
   setValue(option);

+ 3 - 0
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -9,6 +9,8 @@ define([
 function (angular, _, moment, dateMath, kbn, templatingVariable) {
   'use strict';
 
+  kbn = kbn.default;
+
   /** @ngInject */
   function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
     this.type = 'cloudwatch';
@@ -354,6 +356,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable) {
         var t = angular.copy(target);
         var scopedVar = {};
         scopedVar[variable.name] = v;
+        t.refId = target.refId + '_' + v.value;
         t.dimensions[dimensionKey] = templateSrv.replace(t.dimensions[dimensionKey], scopedVar);
         return t;
       }).value();

+ 6 - 0
public/app/plugins/datasource/elasticsearch/datasource.ts

@@ -75,6 +75,12 @@ export class ElasticDatasource {
     return this.request('POST', url, data).then(function(results) {
       results.data.$$config = results.config;
       return results.data;
+    }).catch(err => {
+      if (err.data && err.data.error) {
+        throw {message: 'Elasticsearch error: ' + err.data.error.reason, error: err.data.error};
+      }
+
+      throw err;
     });
   }
 

+ 2 - 0
public/app/plugins/datasource/graphite/add_graphite_func.js

@@ -7,6 +7,8 @@ define([
 function (angular, _, $, gfunc) {
   'use strict';
 
+  gfunc = gfunc.default;
+
   angular
     .module('grafana.directives')
     .directive('graphiteAddFunc', function($compile) {

+ 0 - 2
public/app/plugins/datasource/graphite/gfunc.d.ts

@@ -1,2 +0,0 @@
-declare var test: any;
-export default test;

+ 0 - 986
public/app/plugins/datasource/graphite/gfunc.js

@@ -1,986 +0,0 @@
-define([
-  'lodash',
-  'jquery'
-],
-function (_, $) {
-  'use strict';
-
-  var index = [];
-  var categories = {
-    Combine: [],
-    Transform: [],
-    Calculate: [],
-    Filter: [],
-    Special: []
-  };
-
-  function addFuncDef(funcDef) {
-    funcDef.params = funcDef.params || [];
-    funcDef.defaultParams = funcDef.defaultParams || [];
-
-    if (funcDef.category) {
-      funcDef.category.push(funcDef);
-    }
-    index[funcDef.name] = funcDef;
-    index[funcDef.shortName || funcDef.name] = funcDef;
-  }
-
-  var optionalSeriesRefArgs = [
-    { name: 'other', type: 'value_or_series', optional: true },
-    { name: 'other', type: 'value_or_series', optional: true },
-    { name: 'other', type: 'value_or_series', optional: true },
-    { name: 'other', type: 'value_or_series', optional: true },
-    { name: 'other', type: 'value_or_series', optional: true }
-  ];
-
-  addFuncDef({
-    name: 'scaleToSeconds',
-    category: categories.Transform,
-    params: [{ name: 'seconds', type: 'int' }],
-    defaultParams: [1],
-  });
-
-  addFuncDef({
-    name: 'perSecond',
-    category: categories.Transform,
-    params: [{ name: "max value", type: "int", optional: true }],
-    defaultParams: [],
-  });
-
-  addFuncDef({
-    name: "holtWintersForecast",
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: "holtWintersConfidenceBands",
-    category: categories.Calculate,
-    params: [{ name: "delta", type: 'int' }],
-    defaultParams: [3]
-  });
-
-  addFuncDef({
-    name: "holtWintersAberration",
-    category: categories.Calculate,
-    params: [{ name: "delta", type: 'int' }],
-    defaultParams: [3]
-  });
-
-  addFuncDef({
-    name: "nPercentile",
-    category: categories.Calculate,
-    params: [{ name: "Nth percentile", type: 'int' }],
-    defaultParams: [95]
-  });
-
-  addFuncDef({
-    name: 'diffSeries',
-    params: optionalSeriesRefArgs,
-    defaultParams: ['#A'],
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: 'stddevSeries',
-    params: optionalSeriesRefArgs,
-    defaultParams: [''],
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: 'divideSeries',
-    params: optionalSeriesRefArgs,
-    defaultParams: ['#A'],
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: 'multiplySeries',
-    params: optionalSeriesRefArgs,
-    defaultParams: ['#A'],
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: 'asPercent',
-    params: optionalSeriesRefArgs,
-    defaultParams: ['#A'],
-    category: categories.Calculate,
-  });
-
-  addFuncDef({
-    name: 'group',
-    params: optionalSeriesRefArgs,
-    defaultParams: ['#A', '#B'],
-    category: categories.Combine,
-  });
-
-  addFuncDef({
-    name: 'sumSeries',
-    shortName: 'sum',
-    category: categories.Combine,
-    params: optionalSeriesRefArgs,
-    defaultParams: [''],
-  });
-
-  addFuncDef({
-    name: 'averageSeries',
-    shortName: 'avg',
-    category: categories.Combine,
-    params: optionalSeriesRefArgs,
-    defaultParams: [''],
-  });
-
-  addFuncDef({
-    name: 'rangeOfSeries',
-    category: categories.Combine
-  });
-
-  addFuncDef({
-    name: 'percentileOfSeries',
-    category: categories.Combine,
-    params: [{ name: 'n', type: 'int' }, { name: 'interpolate', type: 'boolean', options: ['true', 'false'] }],
-    defaultParams: [95, 'false']
-  });
-
-  addFuncDef({
-    name: 'sumSeriesWithWildcards',
-    category: categories.Combine,
-    params: [
-      { name: "node", type: "int" },
-      { name: "node", type: "int", optional: true },
-      { name: "node", type: "int", optional: true },
-      { name: "node", type: "int", optional: true }
-    ],
-    defaultParams: [3]
-  });
-
-  addFuncDef({
-    name: 'maxSeries',
-    shortName: 'max',
-    category: categories.Combine,
-  });
-
-  addFuncDef({
-    name: 'minSeries',
-    shortName: 'min',
-    category: categories.Combine,
-  });
-
-  addFuncDef({
-    name: 'averageSeriesWithWildcards',
-    category: categories.Combine,
-    params: [
-      { name: "node", type: "int" },
-      { name: "node", type: "int", optional: true },
-    ],
-    defaultParams: [3]
-  });
-
-  addFuncDef({
-    name: "alias",
-    category: categories.Special,
-    params: [{ name: "alias", type: 'string' }],
-    defaultParams: ['alias']
-  });
-
-  addFuncDef({
-    name: "aliasSub",
-    category: categories.Special,
-    params: [{ name: "search", type: 'string' }, { name: "replace", type: 'string' }],
-    defaultParams: ['', '\\1']
-  });
-
-  addFuncDef({
-    name: "stacked",
-    category: categories.Special,
-    params: [{ name: "stack", type: 'string' }],
-    defaultParams: ['stacked']
-  });
-
-  addFuncDef({
-    name: "consolidateBy",
-    category: categories.Special,
-    params: [
-      {
-        name: 'function',
-        type: 'string',
-        options: ['sum', 'average', 'min', 'max']
-      }
-    ],
-    defaultParams: ['max']
-  });
-
-  addFuncDef({
-    name: "cumulative",
-    category: categories.Special,
-    params: [],
-    defaultParams: []
-  });
-
-  addFuncDef({
-    name: "groupByNode",
-    category: categories.Special,
-    params: [
-      {
-        name: "node",
-        type: "int",
-        options: [0,1,2,3,4,5,6,7,8,9,10,12]
-      },
-      {
-        name: "function",
-        type: "string",
-        options: ['sum', 'avg', 'maxSeries']
-      }
-    ],
-    defaultParams: [3, "sum"]
-  });
-
-  addFuncDef({
-    name: 'aliasByNode',
-    category: categories.Special,
-    params: [
-      { name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-    ],
-    defaultParams: [3]
-  });
-
-  addFuncDef({
-    name: 'substr',
-    category: categories.Special,
-    params: [
-      { name: "start", type: "int", options: [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,12] },
-      { name: "stop", type: "int", options: [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,12] },
-    ],
-    defaultParams: [0, 0]
-  });
-
-  addFuncDef({
-    name: 'sortByName',
-    category: categories.Special,
-    params: [{ name: 'natural', type: 'boolean', options: ['true', 'false'], optional: true }],
-    defaultParams: ['false']
-  });
-
-  addFuncDef({
-    name: 'sortByMaxima',
-    category: categories.Special
-  });
-
-  addFuncDef({
-    name: 'sortByMinima',
-    category: categories.Special
-  });
-
-  addFuncDef({
-    name: 'sortByTotal',
-    category: categories.Special
-  });
-
-  addFuncDef({
-    name: 'aliasByMetric',
-    category: categories.Special,
-  });
-
-  addFuncDef({
-    name: 'randomWalk',
-    fake: true,
-    category: categories.Special,
-    params: [{ name: "name", type: "string", }],
-    defaultParams: ['randomWalk']
-  });
-
-  addFuncDef({
-    name: 'countSeries',
-    category: categories.Special
-  });
-
-  addFuncDef({
-    name: 'constantLine',
-    category: categories.Special,
-    params: [{ name: "value", type: "int", }],
-    defaultParams: [10]
-  });
-
-  addFuncDef({
-    name: 'cactiStyle',
-    category: categories.Special,
-  });
-
-  addFuncDef({
-    name: 'keepLastValue',
-    category: categories.Special,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [100]
-  });
-
-  addFuncDef({
-    name: "changed",
-    category: categories.Special,
-    params: [],
-    defaultParams: []
-  });
-
-  addFuncDef({
-    name: 'scale',
-    category: categories.Transform,
-    params: [{ name: "factor", type: "int", }],
-    defaultParams: [1]
-  });
-
-  addFuncDef({
-    name: 'offset',
-    category: categories.Transform,
-    params: [{ name: "amount", type: "int", }],
-    defaultParams: [10]
-  });
-
-  addFuncDef({
-    name: 'transformNull',
-    category: categories.Transform,
-    params: [{ name: "amount", type: "int", }],
-    defaultParams: [0]
-  });
-
-  addFuncDef({
-    name: 'integral',
-    category: categories.Transform,
-  });
-
-  addFuncDef({
-    name: 'derivative',
-    category: categories.Transform,
-  });
-
-  addFuncDef({
-    name: 'nonNegativeDerivative',
-    category: categories.Transform,
-    params: [{ name: "max value or 0", type: "int", optional: true }],
-    defaultParams: ['']
-  });
-
-  addFuncDef({
-    name: 'timeShift',
-    category: categories.Transform,
-    params: [{ name: "amount", type: "select", options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] }],
-    defaultParams: ['1d']
-  });
-
-  addFuncDef({
-    name: 'timeStack',
-    category: categories.Transform,
-    params: [
-      { name: "timeShiftUnit", type: "select", options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] },
-      { name: "timeShiftStart", type: "int" },
-      { name: "timeShiftEnd", type: "int" }
-    ],
-    defaultParams: ['1d', 0, 7]
-  });
-
-  addFuncDef({
-    name: 'summarize',
-    category: categories.Transform,
-    params: [
-      { name: "interval", type: "string" },
-      { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] },
-      { name: "alignToFrom", type: "boolean", optional: true, options: ['false', 'true'] },
-    ],
-    defaultParams: ['1h', 'sum', 'false']
-  });
-
-  addFuncDef({
-    name: 'smartSummarize',
-    category: categories.Transform,
-    params: [{ name: "interval", type: "string" }, { name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last'] }],
-    defaultParams: ['1h', 'sum']
-  });
-
-  addFuncDef({
-    name: 'absolute',
-    category: categories.Transform,
-  });
-
-  addFuncDef({
-    name: 'hitcount',
-    category: categories.Transform,
-    params: [{ name: "interval", type: "string" }],
-    defaultParams: ['10s']
-  });
-
-  addFuncDef({
-    name: 'log',
-    category: categories.Transform,
-    params: [{ name: "base", type: "int" }],
-    defaultParams: ['10']
-  });
-
-  addFuncDef({
-    name: 'averageAbove',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [25]
-  });
-
-  addFuncDef({
-    name: 'averageBelow',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [25]
-  });
-
-  addFuncDef({
-    name: 'currentAbove',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [25]
-  });
-
-  addFuncDef({
-    name: 'currentBelow',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [25]
-  });
-
-  addFuncDef({
-    name: 'maximumAbove',
-    category: categories.Filter,
-    params: [{ name: "value", type: "int" }],
-    defaultParams: [0]
-  });
-
-  addFuncDef({
-    name: 'maximumBelow',
-    category: categories.Filter,
-    params: [{ name: "value", type: "int" }],
-    defaultParams: [0]
-  });
-
-  addFuncDef({
-    name: 'minimumAbove',
-    category: categories.Filter,
-    params: [{ name: "value", type: "int" }],
-    defaultParams: [0]
-  });
-
-  addFuncDef({
-    name: 'minimumBelow',
-    category: categories.Filter,
-    params: [{ name: "value", type: "int" }],
-    defaultParams: [0]
-  });
-
-  addFuncDef({
-    name: 'limit',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'mostDeviant',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [10]
-  });
-
-  addFuncDef({
-    name: "exclude",
-    category: categories.Filter,
-    params: [{ name: "exclude", type: 'string' }],
-    defaultParams: ['exclude']
-  });
-
-  addFuncDef({
-    name: 'highestCurrent',
-    category: categories.Filter,
-    params: [{ name: "count", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'highestMax',
-    category: categories.Filter,
-    params: [{ name: "count", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'lowestCurrent',
-    category: categories.Filter,
-    params: [{ name: "count", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'movingAverage',
-    category: categories.Filter,
-    params: [{ name: "windowSize", type: "int_or_interval", options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: [10]
-  });
-
-  addFuncDef({
-    name: 'movingMedian',
-    category: categories.Filter,
-    params: [{ name: "windowSize", type: "int_or_interval", options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: ['5']
-  });
-
-  addFuncDef({
-    name: 'stdev',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }, { name: "tolerance", type: "int" }],
-    defaultParams: [5,0.1]
-  });
-
-  addFuncDef({
-    name: 'highestAverage',
-    category: categories.Filter,
-    params: [{ name: "count", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'lowestAverage',
-    category: categories.Filter,
-    params: [{ name: "count", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'removeAbovePercentile',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'removeAboveValue',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'removeBelowPercentile',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'removeBelowValue',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int" }],
-    defaultParams: [5]
-  });
-
-  addFuncDef({
-    name: 'useSeriesAbove',
-    category: categories.Filter,
-    params: [
-      { name: "value", type: "int" },
-      { name: "search", type: "string" },
-      { name: "replace", type: "string" }
-    ],
-    defaultParams: [0, 'search', 'replace']
-  });
-
-  ////////////////////
-  // Graphite 1.0.x //
-  ////////////////////
-
-  addFuncDef({
-    name: 'aggregateLine',
-    category: categories.Combine,
-    params: [{ name: "func", type: "select", options: ['sum', 'avg', 'min', 'max', 'last']}],
-    defaultParams: ['avg'],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'averageOutsidePercentile',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [95],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'delay',
-    category: categories.Transform,
-    params: [{ name: 'steps', type: 'int', }],
-    defaultParams: [1],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'exponentialMovingAverage',
-    category: categories.Calculate,
-    params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: [10],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'fallbackSeries',
-    category: categories.Special,
-    params: [{ name: 'fallback', type: 'string' }],
-    defaultParams: ['constantLine(0)'],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: "grep",
-    category: categories.Filter,
-    params: [{ name: "grep", type: 'string' }],
-    defaultParams: ['grep'],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: "groupByNodes",
-    category: categories.Special,
-    params: [
-      {
-        name: "function",
-        type: "string",
-        options: ['sum', 'avg', 'maxSeries']
-      },
-      { name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "node", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-    ],
-    defaultParams: ["sum", 3],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'integralByInterval',
-    category: categories.Transform,
-    params: [{ name: "intervalUnit", type: "select", options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] }],
-    defaultParams: ['1d'],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'interpolate',
-    category: categories.Transform,
-    params: [{ name: 'limit', type: 'int', optional: true}],
-    defaultParams: [],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'invert',
-    category: categories.Transform,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'isNonNull',
-    category: categories.Combine,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'linearRegression',
-    category: categories.Calculate,
-    params: [
-      { name: "startSourceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true },
-      { name: "endSourceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true }
-    ],
-    defaultParams: [],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'mapSeries',
-    shortName: 'map',
-    params: [{ name: "node", type: 'int' }],
-    defaultParams: [3],
-    category: categories.Combine,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'movingMin',
-    category: categories.Calculate,
-    params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: [10],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'movingMax',
-    category: categories.Calculate,
-    params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: [10],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'movingSum',
-    category: categories.Calculate,
-    params: [{ name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] }],
-    defaultParams: [10],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: "multiplySeriesWithWildcards",
-    category: categories.Calculate,
-    params: [
-      { name: "position", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
-      { name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-      { name: "position", type: "int", options: [0,-1,-2,-3,-4,-5,-6,-7], optional: true },
-    ],
-    defaultParams: [2],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'offsetToZero',
-    category: categories.Transform,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'pow',
-    category: categories.Transform,
-    params: [{ name: 'factor', type: 'int' }],
-    defaultParams: [10],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'powSeries',
-    category: categories.Transform,
-    params: optionalSeriesRefArgs,
-    defaultParams: [''],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'reduceSeries',
-    shortName: 'reduce',
-    params: [
-      { name: "function", type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
-      { name: "reduceNode", type: 'int', options: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] },
-      { name: "reduceMatchers", type: 'string' },
-      { name: "reduceMatchers", type: 'string' },
-    ],
-    defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
-    category: categories.Combine,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'removeBetweenPercentile',
-    category: categories.Filter,
-    params: [{ name: "n", type: "int", }],
-    defaultParams: [95],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'removeEmptySeries',
-    category: categories.Filter,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'squareRoot',
-    category: categories.Transform,
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'timeSlice',
-    category: categories.Transform,
-    params: [
-      { name: "startSliceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d']},
-      { name: "endSliceAt", type: "select", options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'], optional: true }
-    ],
-    defaultParams: ['-1h'],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'weightedAverage',
-    category: categories.Filter,
-    params: [
-      { name: 'other', type: 'value_or_series', optional: true },
-      { name: "node", type: "int", options: [0,1,2,3,4,5,6,7,8,9,10,12] },
-    ],
-    defaultParams: ['#A', 4],
-    version: '1.0'
-  });
-
-  addFuncDef({
-    name: 'seriesByTag',
-    category: categories.Special,
-    params: [
-      { name: "tagExpression", type: "string" },
-      { name: "tagExpression", type: "string", optional: true },
-      { name: "tagExpression", type: "string", optional: true },
-      { name: "tagExpression", type: "string", optional: true },
-    ],
-    version: '1.1'
-  });
-
-  addFuncDef({
-    name: "groupByTags",
-    category: categories.Special,
-    params: [
-      {
-        name: "function",
-        type: "string",
-        options: ['sum', 'avg', 'maxSeries']
-      },
-      { name: "tag", type: "string" },
-      { name: "tag", type: "string", optional: true },
-      { name: "tag", type: "string", optional: true },
-      { name: "tag", type: "string", optional: true },
-    ],
-    defaultParams: ["sum", "tag"],
-    version: '1.1'
-  });
-
-  addFuncDef({
-    name: "aliasByTags",
-    category: categories.Special,
-    params: [
-      { name: "tag", type: "string" },
-      { name: "tag", type: "string", optional: true },
-      { name: "tag", type: "string", optional: true },
-      { name: "tag", type: "string", optional: true },
-    ],
-    defaultParams: ["tag"],
-    version: '1.1'
-  });
-
-  _.each(categories, function(funcList, catName) {
-    categories[catName] = _.sortBy(funcList, 'name');
-  });
-
-  function FuncInstance(funcDef, options) {
-    this.def = funcDef;
-    this.params = [];
-
-    if (options && options.withDefaultParams) {
-      this.params = funcDef.defaultParams.slice(0);
-    }
-
-    this.updateText();
-  }
-
-  FuncInstance.prototype.render = function(metricExp) {
-    var str = this.def.name + '(';
-    var parameters = _.map(this.params, function(value, index) {
-
-      var paramType = this.def.params[index].type;
-      if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
-        return value;
-      }
-      else if (paramType === 'int_or_interval' && $.isNumeric(value)) {
-        return value;
-      }
-
-      return "'" + value + "'";
-
-    }.bind(this));
-
-    if (metricExp) {
-      parameters.unshift(metricExp);
-    }
-
-    return str + parameters.join(', ') + ')';
-  };
-
-  FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
-    if (strValue.indexOf(',') === -1) {
-      return false;
-    }
-
-    return this.def.params[index + 1] && this.def.params[index + 1].optional;
-  };
-
-  FuncInstance.prototype.updateParam = function(strValue, index) {
-    // handle optional parameters
-    // if string contains ',' and next param is optional, split and update both
-    if (this._hasMultipleParamsInString(strValue, index)) {
-      _.each(strValue.split(','), function(partVal, idx) {
-        this.updateParam(partVal.trim(), index + idx);
-      }.bind(this));
-      return;
-    }
-
-    if (strValue === '' && this.def.params[index].optional) {
-      this.params.splice(index, 1);
-    }
-    else {
-      this.params[index] = strValue;
-    }
-
-    this.updateText();
-  };
-
-  FuncInstance.prototype.updateText = function () {
-    if (this.params.length === 0) {
-      this.text = this.def.name + '()';
-      return;
-    }
-
-    var text = this.def.name + '(';
-    text += this.params.join(', ');
-    text += ')';
-    this.text = text;
-  };
-
-  function isVersionRelatedFunction(func, graphiteVersion) {
-    return isVersionGreaterOrEqual(graphiteVersion, func.version) || !func.version;
-  }
-
-  function isVersionGreaterOrEqual(a, b) {
-    var a_num = Number(a);
-    var b_num = Number(b);
-    return a_num >= b_num;
-  }
-
-  return {
-    createFuncInstance: function(funcDef, options) {
-      if (_.isString(funcDef)) {
-        if (!index[funcDef]) {
-          throw { message: 'Method not found ' + name };
-        }
-        funcDef = index[funcDef];
-      }
-      return new FuncInstance(funcDef, options);
-    },
-
-    getFuncDef: function(name) {
-      return index[name];
-    },
-
-    getCategories: function(graphiteVersion) {
-      var filteredCategories = {};
-      _.each(categories, function(functions, category) {
-        var filteredFuncs = _.filter(functions, function(func) {
-          return isVersionRelatedFunction(func, graphiteVersion);
-        });
-        if (filteredFuncs.length) {
-          filteredCategories[category] = filteredFuncs;
-        }
-      });
-
-      return filteredCategories;
-    }
-  };
-
-});

+ 1008 - 0
public/app/plugins/datasource/graphite/gfunc.ts

@@ -0,0 +1,1008 @@
+import _ from 'lodash';
+
+var index = [];
+var categories = {
+  Combine: [],
+  Transform: [],
+  Calculate: [],
+  Filter: [],
+  Special: [],
+};
+
+function addFuncDef(funcDef) {
+  funcDef.params = funcDef.params || [];
+  funcDef.defaultParams = funcDef.defaultParams || [];
+
+  if (funcDef.category) {
+    funcDef.category.push(funcDef);
+  }
+  index[funcDef.name] = funcDef;
+  index[funcDef.shortName || funcDef.name] = funcDef;
+}
+
+var optionalSeriesRefArgs = [
+  { name: 'other', type: 'value_or_series', optional: true },
+  { name: 'other', type: 'value_or_series', optional: true },
+  { name: 'other', type: 'value_or_series', optional: true },
+  { name: 'other', type: 'value_or_series', optional: true },
+  { name: 'other', type: 'value_or_series', optional: true },
+];
+
+addFuncDef({
+  name: 'scaleToSeconds',
+  category: categories.Transform,
+  params: [{ name: 'seconds', type: 'int' }],
+  defaultParams: [1],
+});
+
+addFuncDef({
+  name: 'perSecond',
+  category: categories.Transform,
+  params: [{ name: 'max value', type: 'int', optional: true }],
+  defaultParams: [],
+});
+
+addFuncDef({
+  name: 'holtWintersForecast',
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'holtWintersConfidenceBands',
+  category: categories.Calculate,
+  params: [{ name: 'delta', type: 'int' }],
+  defaultParams: [3],
+});
+
+addFuncDef({
+  name: 'holtWintersAberration',
+  category: categories.Calculate,
+  params: [{ name: 'delta', type: 'int' }],
+  defaultParams: [3],
+});
+
+addFuncDef({
+  name: 'nPercentile',
+  category: categories.Calculate,
+  params: [{ name: 'Nth percentile', type: 'int' }],
+  defaultParams: [95],
+});
+
+addFuncDef({
+  name: 'diffSeries',
+  params: optionalSeriesRefArgs,
+  defaultParams: ['#A'],
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'stddevSeries',
+  params: optionalSeriesRefArgs,
+  defaultParams: [''],
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'divideSeries',
+  params: optionalSeriesRefArgs,
+  defaultParams: ['#A'],
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'multiplySeries',
+  params: optionalSeriesRefArgs,
+  defaultParams: ['#A'],
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'asPercent',
+  params: optionalSeriesRefArgs,
+  defaultParams: ['#A'],
+  category: categories.Calculate,
+});
+
+addFuncDef({
+  name: 'group',
+  params: optionalSeriesRefArgs,
+  defaultParams: ['#A', '#B'],
+  category: categories.Combine,
+});
+
+addFuncDef({
+  name: 'sumSeries',
+  shortName: 'sum',
+  category: categories.Combine,
+  params: optionalSeriesRefArgs,
+  defaultParams: [''],
+});
+
+addFuncDef({
+  name: 'averageSeries',
+  shortName: 'avg',
+  category: categories.Combine,
+  params: optionalSeriesRefArgs,
+  defaultParams: [''],
+});
+
+addFuncDef({
+  name: 'rangeOfSeries',
+  category: categories.Combine,
+});
+
+addFuncDef({
+  name: 'percentileOfSeries',
+  category: categories.Combine,
+  params: [{ name: 'n', type: 'int' }, { name: 'interpolate', type: 'boolean', options: ['true', 'false'] }],
+  defaultParams: [95, 'false'],
+});
+
+addFuncDef({
+  name: 'sumSeriesWithWildcards',
+  category: categories.Combine,
+  params: [
+    { name: 'node', type: 'int' },
+    { name: 'node', type: 'int', optional: true },
+    { name: 'node', type: 'int', optional: true },
+    { name: 'node', type: 'int', optional: true },
+  ],
+  defaultParams: [3],
+});
+
+addFuncDef({
+  name: 'maxSeries',
+  shortName: 'max',
+  category: categories.Combine,
+});
+
+addFuncDef({
+  name: 'minSeries',
+  shortName: 'min',
+  category: categories.Combine,
+});
+
+addFuncDef({
+  name: 'averageSeriesWithWildcards',
+  category: categories.Combine,
+  params: [{ name: 'node', type: 'int' }, { name: 'node', type: 'int', optional: true }],
+  defaultParams: [3],
+});
+
+addFuncDef({
+  name: 'alias',
+  category: categories.Special,
+  params: [{ name: 'alias', type: 'string' }],
+  defaultParams: ['alias'],
+});
+
+addFuncDef({
+  name: 'aliasSub',
+  category: categories.Special,
+  params: [{ name: 'search', type: 'string' }, { name: 'replace', type: 'string' }],
+  defaultParams: ['', '\\1'],
+});
+
+addFuncDef({
+  name: 'stacked',
+  category: categories.Special,
+  params: [{ name: 'stack', type: 'string' }],
+  defaultParams: ['stacked'],
+});
+
+addFuncDef({
+  name: 'consolidateBy',
+  category: categories.Special,
+  params: [
+    {
+      name: 'function',
+      type: 'string',
+      options: ['sum', 'average', 'min', 'max'],
+    },
+  ],
+  defaultParams: ['max'],
+});
+
+addFuncDef({
+  name: 'cumulative',
+  category: categories.Special,
+  params: [],
+  defaultParams: [],
+});
+
+addFuncDef({
+  name: 'groupByNode',
+  category: categories.Special,
+  params: [
+    {
+      name: 'node',
+      type: 'int',
+      options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12],
+    },
+    {
+      name: 'function',
+      type: 'string',
+      options: ['sum', 'avg', 'maxSeries'],
+    },
+  ],
+  defaultParams: [3, 'sum'],
+});
+
+addFuncDef({
+  name: 'aliasByNode',
+  category: categories.Special,
+  params: [
+    { name: 'node', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+  ],
+  defaultParams: [3],
+});
+
+addFuncDef({
+  name: 'substr',
+  category: categories.Special,
+  params: [
+    { name: 'start', type: 'int', options: [-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+    { name: 'stop', type: 'int', options: [-6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+  ],
+  defaultParams: [0, 0],
+});
+
+addFuncDef({
+  name: 'sortByName',
+  category: categories.Special,
+  params: [{ name: 'natural', type: 'boolean', options: ['true', 'false'], optional: true }],
+  defaultParams: ['false'],
+});
+
+addFuncDef({
+  name: 'sortByMaxima',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'sortByMinima',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'sortByTotal',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'aliasByMetric',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'randomWalk',
+  fake: true,
+  category: categories.Special,
+  params: [{ name: 'name', type: 'string' }],
+  defaultParams: ['randomWalk'],
+});
+
+addFuncDef({
+  name: 'countSeries',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'constantLine',
+  category: categories.Special,
+  params: [{ name: 'value', type: 'int' }],
+  defaultParams: [10],
+});
+
+addFuncDef({
+  name: 'cactiStyle',
+  category: categories.Special,
+});
+
+addFuncDef({
+  name: 'keepLastValue',
+  category: categories.Special,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [100],
+});
+
+addFuncDef({
+  name: 'changed',
+  category: categories.Special,
+  params: [],
+  defaultParams: [],
+});
+
+addFuncDef({
+  name: 'scale',
+  category: categories.Transform,
+  params: [{ name: 'factor', type: 'int' }],
+  defaultParams: [1],
+});
+
+addFuncDef({
+  name: 'offset',
+  category: categories.Transform,
+  params: [{ name: 'amount', type: 'int' }],
+  defaultParams: [10],
+});
+
+addFuncDef({
+  name: 'transformNull',
+  category: categories.Transform,
+  params: [{ name: 'amount', type: 'int' }],
+  defaultParams: [0],
+});
+
+addFuncDef({
+  name: 'integral',
+  category: categories.Transform,
+});
+
+addFuncDef({
+  name: 'derivative',
+  category: categories.Transform,
+});
+
+addFuncDef({
+  name: 'nonNegativeDerivative',
+  category: categories.Transform,
+  params: [{ name: 'max value or 0', type: 'int', optional: true }],
+  defaultParams: [''],
+});
+
+addFuncDef({
+  name: 'timeShift',
+  category: categories.Transform,
+  params: [{ name: 'amount', type: 'select', options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] }],
+  defaultParams: ['1d'],
+});
+
+addFuncDef({
+  name: 'timeStack',
+  category: categories.Transform,
+  params: [
+    { name: 'timeShiftUnit', type: 'select', options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] },
+    { name: 'timeShiftStart', type: 'int' },
+    { name: 'timeShiftEnd', type: 'int' },
+  ],
+  defaultParams: ['1d', 0, 7],
+});
+
+addFuncDef({
+  name: 'summarize',
+  category: categories.Transform,
+  params: [
+    { name: 'interval', type: 'string' },
+    { name: 'func', type: 'select', options: ['sum', 'avg', 'min', 'max', 'last'] },
+    { name: 'alignToFrom', type: 'boolean', optional: true, options: ['false', 'true'] },
+  ],
+  defaultParams: ['1h', 'sum', 'false'],
+});
+
+addFuncDef({
+  name: 'smartSummarize',
+  category: categories.Transform,
+  params: [
+    { name: 'interval', type: 'string' },
+    { name: 'func', type: 'select', options: ['sum', 'avg', 'min', 'max', 'last'] },
+  ],
+  defaultParams: ['1h', 'sum'],
+});
+
+addFuncDef({
+  name: 'absolute',
+  category: categories.Transform,
+});
+
+addFuncDef({
+  name: 'hitcount',
+  category: categories.Transform,
+  params: [{ name: 'interval', type: 'string' }],
+  defaultParams: ['10s'],
+});
+
+addFuncDef({
+  name: 'log',
+  category: categories.Transform,
+  params: [{ name: 'base', type: 'int' }],
+  defaultParams: ['10'],
+});
+
+addFuncDef({
+  name: 'averageAbove',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [25],
+});
+
+addFuncDef({
+  name: 'averageBelow',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [25],
+});
+
+addFuncDef({
+  name: 'currentAbove',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [25],
+});
+
+addFuncDef({
+  name: 'currentBelow',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [25],
+});
+
+addFuncDef({
+  name: 'maximumAbove',
+  category: categories.Filter,
+  params: [{ name: 'value', type: 'int' }],
+  defaultParams: [0],
+});
+
+addFuncDef({
+  name: 'maximumBelow',
+  category: categories.Filter,
+  params: [{ name: 'value', type: 'int' }],
+  defaultParams: [0],
+});
+
+addFuncDef({
+  name: 'minimumAbove',
+  category: categories.Filter,
+  params: [{ name: 'value', type: 'int' }],
+  defaultParams: [0],
+});
+
+addFuncDef({
+  name: 'minimumBelow',
+  category: categories.Filter,
+  params: [{ name: 'value', type: 'int' }],
+  defaultParams: [0],
+});
+
+addFuncDef({
+  name: 'limit',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'mostDeviant',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [10],
+});
+
+addFuncDef({
+  name: 'exclude',
+  category: categories.Filter,
+  params: [{ name: 'exclude', type: 'string' }],
+  defaultParams: ['exclude'],
+});
+
+addFuncDef({
+  name: 'highestCurrent',
+  category: categories.Filter,
+  params: [{ name: 'count', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'highestMax',
+  category: categories.Filter,
+  params: [{ name: 'count', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'lowestCurrent',
+  category: categories.Filter,
+  params: [{ name: 'count', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'movingAverage',
+  category: categories.Filter,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: [10],
+});
+
+addFuncDef({
+  name: 'movingMedian',
+  category: categories.Filter,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: ['5'],
+});
+
+addFuncDef({
+  name: 'stdev',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }, { name: 'tolerance', type: 'int' }],
+  defaultParams: [5, 0.1],
+});
+
+addFuncDef({
+  name: 'highestAverage',
+  category: categories.Filter,
+  params: [{ name: 'count', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'lowestAverage',
+  category: categories.Filter,
+  params: [{ name: 'count', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'removeAbovePercentile',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'removeAboveValue',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'removeBelowPercentile',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'removeBelowValue',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [5],
+});
+
+addFuncDef({
+  name: 'useSeriesAbove',
+  category: categories.Filter,
+  params: [{ name: 'value', type: 'int' }, { name: 'search', type: 'string' }, { name: 'replace', type: 'string' }],
+  defaultParams: [0, 'search', 'replace'],
+});
+
+////////////////////
+// Graphite 1.0.x //
+////////////////////
+
+addFuncDef({
+  name: 'aggregateLine',
+  category: categories.Combine,
+  params: [{ name: 'func', type: 'select', options: ['sum', 'avg', 'min', 'max', 'last'] }],
+  defaultParams: ['avg'],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'averageOutsidePercentile',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [95],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'delay',
+  category: categories.Transform,
+  params: [{ name: 'steps', type: 'int' }],
+  defaultParams: [1],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'exponentialMovingAverage',
+  category: categories.Calculate,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: [10],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'fallbackSeries',
+  category: categories.Special,
+  params: [{ name: 'fallback', type: 'string' }],
+  defaultParams: ['constantLine(0)'],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'grep',
+  category: categories.Filter,
+  params: [{ name: 'grep', type: 'string' }],
+  defaultParams: ['grep'],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'groupByNodes',
+  category: categories.Special,
+  params: [
+    {
+      name: 'function',
+      type: 'string',
+      options: ['sum', 'avg', 'maxSeries'],
+    },
+    { name: 'node', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'node', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+  ],
+  defaultParams: ['sum', 3],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'integralByInterval',
+  category: categories.Transform,
+  params: [{ name: 'intervalUnit', type: 'select', options: ['1h', '6h', '12h', '1d', '2d', '7d', '14d', '30d'] }],
+  defaultParams: ['1d'],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'interpolate',
+  category: categories.Transform,
+  params: [{ name: 'limit', type: 'int', optional: true }],
+  defaultParams: [],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'invert',
+  category: categories.Transform,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'isNonNull',
+  category: categories.Combine,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'linearRegression',
+  category: categories.Calculate,
+  params: [
+    {
+      name: 'startSourceAt',
+      type: 'select',
+      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
+      optional: true,
+    },
+    {
+      name: 'endSourceAt',
+      type: 'select',
+      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
+      optional: true,
+    },
+  ],
+  defaultParams: [],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'mapSeries',
+  shortName: 'map',
+  params: [{ name: 'node', type: 'int' }],
+  defaultParams: [3],
+  category: categories.Combine,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'movingMin',
+  category: categories.Calculate,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: [10],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'movingMax',
+  category: categories.Calculate,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: [10],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'movingSum',
+  category: categories.Calculate,
+  params: [
+    { name: 'windowSize', type: 'int_or_interval', options: ['5', '7', '10', '5min', '10min', '30min', '1hour'] },
+  ],
+  defaultParams: [10],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'multiplySeriesWithWildcards',
+  category: categories.Calculate,
+  params: [
+    { name: 'position', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+    { name: 'position', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'position', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+    { name: 'position', type: 'int', options: [0, -1, -2, -3, -4, -5, -6, -7], optional: true },
+  ],
+  defaultParams: [2],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'offsetToZero',
+  category: categories.Transform,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'pow',
+  category: categories.Transform,
+  params: [{ name: 'factor', type: 'int' }],
+  defaultParams: [10],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'powSeries',
+  category: categories.Transform,
+  params: optionalSeriesRefArgs,
+  defaultParams: [''],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'reduceSeries',
+  shortName: 'reduce',
+  params: [
+    { name: 'function', type: 'string', options: ['asPercent', 'diffSeries', 'divideSeries'] },
+    { name: 'reduceNode', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13] },
+    { name: 'reduceMatchers', type: 'string' },
+    { name: 'reduceMatchers', type: 'string' },
+  ],
+  defaultParams: ['asPercent', 2, 'used_bytes', 'total_bytes'],
+  category: categories.Combine,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'removeBetweenPercentile',
+  category: categories.Filter,
+  params: [{ name: 'n', type: 'int' }],
+  defaultParams: [95],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'removeEmptySeries',
+  category: categories.Filter,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'squareRoot',
+  category: categories.Transform,
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'timeSlice',
+  category: categories.Transform,
+  params: [
+    { name: 'startSliceAt', type: 'select', options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'] },
+    {
+      name: 'endSliceAt',
+      type: 'select',
+      options: ['-1h', '-6h', '-12h', '-1d', '-2d', '-7d', '-14d', '-30d'],
+      optional: true,
+    },
+  ],
+  defaultParams: ['-1h'],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'weightedAverage',
+  category: categories.Filter,
+  params: [
+    { name: 'other', type: 'value_or_series', optional: true },
+    { name: 'node', type: 'int', options: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 12] },
+  ],
+  defaultParams: ['#A', 4],
+  version: '1.0',
+});
+
+addFuncDef({
+  name: 'seriesByTag',
+  category: categories.Special,
+  params: [
+    { name: 'tagExpression', type: 'string' },
+    { name: 'tagExpression', type: 'string', optional: true },
+    { name: 'tagExpression', type: 'string', optional: true },
+    { name: 'tagExpression', type: 'string', optional: true },
+  ],
+  version: '1.1',
+});
+
+addFuncDef({
+  name: 'groupByTags',
+  category: categories.Special,
+  params: [
+    {
+      name: 'function',
+      type: 'string',
+      options: ['sum', 'avg', 'maxSeries'],
+    },
+    { name: 'tag', type: 'string' },
+    { name: 'tag', type: 'string', optional: true },
+    { name: 'tag', type: 'string', optional: true },
+    { name: 'tag', type: 'string', optional: true },
+  ],
+  defaultParams: ['sum', 'tag'],
+  version: '1.1',
+});
+
+addFuncDef({
+  name: 'aliasByTags',
+  category: categories.Special,
+  params: [
+    { name: 'tag', type: 'string' },
+    { name: 'tag', type: 'string', optional: true },
+    { name: 'tag', type: 'string', optional: true },
+    { name: 'tag', type: 'string', optional: true },
+  ],
+  defaultParams: ['tag'],
+  version: '1.1',
+});
+
+_.each(categories, function(funcList, catName) {
+  categories[catName] = _.sortBy(funcList, 'name');
+});
+
+function FuncInstance(funcDef, options?) {
+  this.def = funcDef;
+  this.params = [];
+
+  if (options && options.withDefaultParams) {
+    this.params = funcDef.defaultParams.slice(0);
+  }
+
+  this.updateText();
+}
+
+function isNumeric(obj) {
+  return !isNaN(parseFloat(obj)) && isFinite(obj);
+}
+
+FuncInstance.prototype.render = function(metricExp) {
+  var str = this.def.name + '(';
+  var parameters = _.map(
+    this.params,
+    function(value, index) {
+      var paramType = this.def.params[index].type;
+      if (paramType === 'int' || paramType === 'value_or_series' || paramType === 'boolean') {
+        return value;
+      } else if (paramType === 'int_or_interval' && isNumeric(value)) {
+        return value;
+      }
+
+      return "'" + value + "'";
+    }.bind(this),
+  );
+
+  if (metricExp) {
+    parameters.unshift(metricExp);
+  }
+
+  return str + parameters.join(', ') + ')';
+};
+
+FuncInstance.prototype._hasMultipleParamsInString = function(strValue, index) {
+  if (strValue.indexOf(',') === -1) {
+    return false;
+  }
+
+  return this.def.params[index + 1] && this.def.params[index + 1].optional;
+};
+
+FuncInstance.prototype.updateParam = function(strValue, index) {
+  // handle optional parameters
+  // if string contains ',' and next param is optional, split and update both
+  if (this._hasMultipleParamsInString(strValue, index)) {
+    _.each(
+      strValue.split(','),
+      function(partVal, idx) {
+        this.updateParam(partVal.trim(), index + idx);
+      }.bind(this),
+    );
+    return;
+  }
+
+  if (strValue === '' && this.def.params[index].optional) {
+    this.params.splice(index, 1);
+  } else {
+    this.params[index] = strValue;
+  }
+
+  this.updateText();
+};
+
+FuncInstance.prototype.updateText = function() {
+  if (this.params.length === 0) {
+    this.text = this.def.name + '()';
+    return;
+  }
+
+  var text = this.def.name + '(';
+  text += this.params.join(', ');
+  text += ')';
+  this.text = text;
+};
+
+function isVersionRelatedFunction(func, graphiteVersion) {
+  return isVersionGreaterOrEqual(graphiteVersion, func.version) || !func.version;
+}
+
+function isVersionGreaterOrEqual(a, b) {
+  var a_num = Number(a);
+  var b_num = Number(b);
+  return a_num >= b_num;
+}
+
+export default {
+  createFuncInstance: function(funcDef, options?) {
+    if (_.isString(funcDef)) {
+      if (!index[funcDef]) {
+        throw { message: 'Method not found ' + name };
+      }
+      funcDef = index[funcDef];
+    }
+    return new FuncInstance(funcDef, options);
+  },
+
+  getFuncDef: function(name) {
+    return index[name];
+  },
+
+  getCategories: function(graphiteVersion) {
+    var filteredCategories: any = {};
+    _.each(categories, function(functions, category) {
+      var filteredFuncs = _.filter(functions, function(func) {
+        return isVersionRelatedFunction(func, graphiteVersion);
+      });
+      if (filteredFuncs.length) {
+        filteredCategories[category] = filteredFuncs;
+      }
+    });
+
+    return filteredCategories;
+  },
+};

+ 26 - 28
public/app/plugins/datasource/graphite/specs/gfunc_specs.ts → public/app/plugins/datasource/graphite/specs/gfunc.jest.ts

@@ -1,25 +1,23 @@
-
-import {describe, it, expect} from 'test/lib/common';
 import gfunc from '../gfunc';
 
 describe('when creating func instance from func names', function() {
   it('should return func instance', function() {
     var func = gfunc.createFuncInstance('sumSeries');
-    expect(func).to.be.ok();
-    expect(func.def.name).to.equal('sumSeries');
-    expect(func.def.params.length).to.equal(5);
-    expect(func.def.defaultParams.length).to.equal(1);
+    expect(func).toBeTruthy();
+    expect(func.def.name).toEqual('sumSeries');
+    expect(func.def.params.length).toEqual(5);
+    expect(func.def.defaultParams.length).toEqual(1);
   });
 
   it('should return func instance with shortName', function() {
     var func = gfunc.createFuncInstance('sum');
-    expect(func).to.be.ok();
+    expect(func).toBeTruthy();
   });
 
   it('should return func instance from funcDef', function() {
     var func = gfunc.createFuncInstance('sum');
     var func2 = gfunc.createFuncInstance(func.def);
-    expect(func2).to.be.ok();
+    expect(func2).toBeTruthy();
   });
 
   it('func instance should have text representation', function() {
@@ -27,7 +25,7 @@ describe('when creating func instance from func names', function() {
     func.params[0] = 5;
     func.params[1] = 'avg';
     func.updateText();
-    expect(func.text).to.equal("groupByNode(5, avg)");
+    expect(func.text).toEqual("groupByNode(5, avg)");
   });
 });
 
@@ -35,51 +33,51 @@ describe('when rendering func instance', function() {
 
   it('should handle single metric param', function() {
     var func = gfunc.createFuncInstance('sumSeries');
-    expect(func.render('hello.metric')).to.equal("sumSeries(hello.metric)");
+    expect(func.render('hello.metric')).toEqual("sumSeries(hello.metric)");
   });
 
   it('should include default params if options enable it', function() {
     var func = gfunc.createFuncInstance('scaleToSeconds', { withDefaultParams: true });
-    expect(func.render('hello')).to.equal("scaleToSeconds(hello, 1)");
+    expect(func.render('hello')).toEqual("scaleToSeconds(hello, 1)");
   });
 
   it('should handle int or interval params with number', function() {
     var func = gfunc.createFuncInstance('movingMedian');
     func.params[0] = '5';
-    expect(func.render('hello')).to.equal("movingMedian(hello, 5)");
+    expect(func.render('hello')).toEqual("movingMedian(hello, 5)");
   });
 
   it('should handle int or interval params with interval string', function() {
     var func = gfunc.createFuncInstance('movingMedian');
     func.params[0] = '5min';
-    expect(func.render('hello')).to.equal("movingMedian(hello, '5min')");
+    expect(func.render('hello')).toEqual("movingMedian(hello, '5min')");
   });
 
   it('should handle metric param and int param and string param', function() {
     var func = gfunc.createFuncInstance('groupByNode');
     func.params[0] = 5;
     func.params[1] = 'avg';
-    expect(func.render('hello.metric')).to.equal("groupByNode(hello.metric, 5, 'avg')");
+    expect(func.render('hello.metric')).toEqual("groupByNode(hello.metric, 5, 'avg')");
   });
 
   it('should handle function with no metric param', function() {
     var func = gfunc.createFuncInstance('randomWalk');
     func.params[0] = 'test';
-    expect(func.render(undefined)).to.equal("randomWalk('test')");
+    expect(func.render(undefined)).toEqual("randomWalk('test')");
   });
 
   it('should handle function multiple series params', function() {
     var func = gfunc.createFuncInstance('asPercent');
     func.params[0] = '#B';
-    expect(func.render('#A')).to.equal("asPercent(#A, #B)");
+    expect(func.render('#A')).toEqual("asPercent(#A, #B)");
   });
 
 });
 
 describe('when requesting function categories', function() {
   it('should return function categories', function() {
-    var catIndex = gfunc.getCategories();
-    expect(catIndex.Special.length).to.be.greaterThan(8);
+    var catIndex = gfunc.getCategories('1.0');
+    expect(catIndex.Special.length).toBeGreaterThan(8);
   });
 });
 
@@ -87,14 +85,14 @@ describe('when updating func param', function() {
   it('should update param value and update text representation', function() {
     var func = gfunc.createFuncInstance('summarize', { withDefaultParams: true });
     func.updateParam('1h', 0);
-    expect(func.params[0]).to.be('1h');
-    expect(func.text).to.be('summarize(1h, sum, false)');
+    expect(func.params[0]).toBe('1h');
+    expect(func.text).toBe('summarize(1h, sum, false)');
   });
 
   it('should parse numbers as float', function() {
     var func = gfunc.createFuncInstance('scale');
     func.updateParam('0.001', 0);
-    expect(func.params[0]).to.be('0.001');
+    expect(func.params[0]).toBe('0.001');
   });
 });
 
@@ -102,24 +100,24 @@ describe('when updating func param with optional second parameter', function() {
   it('should update value and text', function() {
     var func = gfunc.createFuncInstance('aliasByNode');
     func.updateParam('1', 0);
-    expect(func.params[0]).to.be('1');
+    expect(func.params[0]).toBe('1');
   });
 
   it('should slit text and put value in second param', function() {
     var func = gfunc.createFuncInstance('aliasByNode');
     func.updateParam('4,-5', 0);
-    expect(func.params[0]).to.be('4');
-    expect(func.params[1]).to.be('-5');
-    expect(func.text).to.be('aliasByNode(4, -5)');
+    expect(func.params[0]).toBe('4');
+    expect(func.params[1]).toBe('-5');
+    expect(func.text).toBe('aliasByNode(4, -5)');
   });
 
   it('should remove second param when empty string is set', function() {
     var func = gfunc.createFuncInstance('aliasByNode');
     func.updateParam('4,-5', 0);
     func.updateParam('', 1);
-    expect(func.params[0]).to.be('4');
-    expect(func.params[1]).to.be(undefined);
-    expect(func.text).to.be('aliasByNode(4)');
+    expect(func.params[0]).toBe('4');
+    expect(func.params[1]).toBe(undefined);
+    expect(func.text).toBe('aliasByNode(4)');
   });
 });
 

+ 50 - 52
public/app/plugins/datasource/graphite/specs/lexer_specs.ts → public/app/plugins/datasource/graphite/specs/lexer.jest.ts

@@ -1,5 +1,3 @@
-
-import {describe, it, expect} from 'test/lib/common';
 import {Lexer} from '../lexer';
 
 describe('when lexing graphite expression', function() {
@@ -7,120 +5,120 @@ describe('when lexing graphite expression', function() {
   it('should tokenize metric expression', function() {
     var lexer = new Lexer('metric.test.*.asd.count');
     var tokens = lexer.tokenize();
-    expect(tokens[0].value).to.be('metric');
-    expect(tokens[1].value).to.be('.');
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[4].type).to.be('identifier');
-    expect(tokens[4].pos).to.be(13);
+    expect(tokens[0].value).toBe('metric');
+    expect(tokens[1].value).toBe('.');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[4].type).toBe('identifier');
+    expect(tokens[4].pos).toBe(13);
   });
 
   it('should tokenize metric expression with dash', function() {
     var lexer = new Lexer('metric.test.se1-server-*.asd.count');
     var tokens = lexer.tokenize();
-    expect(tokens[4].type).to.be('identifier');
-    expect(tokens[4].value).to.be('se1-server-*');
+    expect(tokens[4].type).toBe('identifier');
+    expect(tokens[4].value).toBe('se1-server-*');
   });
 
   it('should tokenize metric expression with dash2', function() {
     var lexer = new Lexer('net.192-168-1-1.192-168-1-9.ping_value.*');
     var tokens = lexer.tokenize();
-    expect(tokens[0].value).to.be('net');
-    expect(tokens[2].value).to.be('192-168-1-1');
+    expect(tokens[0].value).toBe('net');
+    expect(tokens[2].value).toBe('192-168-1-1');
   });
 
   it('should tokenize metric expression with equal sign', function() {
     var lexer = new Lexer('apps=test');
     var tokens = lexer.tokenize();
-    expect(tokens[0].value).to.be('apps=test');
+    expect(tokens[0].value).toBe('apps=test');
   });
 
   it('simple function2', function() {
     var lexer = new Lexer('offset(test.metric, -100)');
     var tokens = lexer.tokenize();
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[4].type).to.be('identifier');
-    expect(tokens[6].type).to.be('number');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[4].type).toBe('identifier');
+    expect(tokens[6].type).toBe('number');
   });
 
   it('should tokenize metric expression with curly braces', function() {
     var lexer = new Lexer('metric.se1-{first, second}.count');
     var tokens = lexer.tokenize();
-    expect(tokens.length).to.be(10);
-    expect(tokens[3].type).to.be('{');
-    expect(tokens[4].value).to.be('first');
-    expect(tokens[5].value).to.be(',');
-    expect(tokens[6].value).to.be('second');
+    expect(tokens.length).toBe(10);
+    expect(tokens[3].type).toBe('{');
+    expect(tokens[4].value).toBe('first');
+    expect(tokens[5].value).toBe(',');
+    expect(tokens[6].value).toBe('second');
   });
 
   it('should tokenize metric expression with number segments', function() {
     var lexer = new Lexer("metric.10.12_10.test");
     var tokens = lexer.tokenize();
-    expect(tokens[0].type).to.be('identifier');
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[2].value).to.be('10');
-    expect(tokens[4].value).to.be('12_10');
-    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[0].type).toBe('identifier');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[2].value).toBe('10');
+    expect(tokens[4].value).toBe('12_10');
+    expect(tokens[4].type).toBe('identifier');
   });
 
   it('should tokenize metric expression with segment that start with number', function() {
     var lexer = new Lexer("metric.001-server");
     var tokens = lexer.tokenize();
-    expect(tokens[0].type).to.be('identifier');
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens.length).to.be(3);
+    expect(tokens[0].type).toBe('identifier');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens.length).toBe(3);
   });
 
   it('should tokenize func call with numbered metric and number arg', function() {
     var lexer = new Lexer("scale(metric.10, 15)");
     var tokens = lexer.tokenize();
-    expect(tokens[0].type).to.be('identifier');
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[2].value).to.be('metric');
-    expect(tokens[4].value).to.be('10');
-    expect(tokens[4].type).to.be('number');
-    expect(tokens[6].type).to.be('number');
+    expect(tokens[0].type).toBe('identifier');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[2].value).toBe('metric');
+    expect(tokens[4].value).toBe('10');
+    expect(tokens[4].type).toBe('number');
+    expect(tokens[6].type).toBe('number');
   });
 
   it('should tokenize metric with template parameter', function() {
     var lexer = new Lexer("metric.[[server]].test");
     var tokens = lexer.tokenize();
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[2].value).to.be('[[server]]');
-    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[2].value).toBe('[[server]]');
+    expect(tokens[4].type).toBe('identifier');
   });
 
   it('should tokenize metric with question mark', function() {
     var lexer = new Lexer("metric.server_??.test");
     var tokens = lexer.tokenize();
-    expect(tokens[2].type).to.be('identifier');
-    expect(tokens[2].value).to.be('server_??');
-    expect(tokens[4].type).to.be('identifier');
+    expect(tokens[2].type).toBe('identifier');
+    expect(tokens[2].value).toBe('server_??');
+    expect(tokens[4].type).toBe('identifier');
   });
 
   it('should handle error with unterminated string', function() {
     var lexer = new Lexer("alias(metric, 'asd)");
     var tokens = lexer.tokenize();
-    expect(tokens[0].value).to.be('alias');
-    expect(tokens[1].value).to.be('(');
-    expect(tokens[2].value).to.be('metric');
-    expect(tokens[3].value).to.be(',');
-    expect(tokens[4].type).to.be('string');
-    expect(tokens[4].isUnclosed).to.be(true);
-    expect(tokens[4].pos).to.be(20);
+    expect(tokens[0].value).toBe('alias');
+    expect(tokens[1].value).toBe('(');
+    expect(tokens[2].value).toBe('metric');
+    expect(tokens[3].value).toBe(',');
+    expect(tokens[4].type).toBe('string');
+    expect(tokens[4].isUnclosed).toBe(true);
+    expect(tokens[4].pos).toBe(20);
   });
 
   it('should handle float parameters', function() {
     var lexer = new Lexer("alias(metric, 0.002)");
     var tokens = lexer.tokenize();
-    expect(tokens[4].type).to.be('number');
-    expect(tokens[4].value).to.be('0.002');
+    expect(tokens[4].type).toBe('number');
+    expect(tokens[4].value).toBe('0.002');
   });
 
   it('should handle bool parameters', function() {
     var lexer = new Lexer("alias(metric, true, false)");
     var tokens = lexer.tokenize();
-    expect(tokens[4].type).to.be('bool');
-    expect(tokens[4].value).to.be('true');
-    expect(tokens[6].type).to.be('bool');
+    expect(tokens[4].type).toBe('bool');
+    expect(tokens[4].value).toBe('true');
+    expect(tokens[6].type).toBe('bool');
   });
 });

+ 182 - 0
public/app/plugins/datasource/graphite/specs/parser.jest.ts

@@ -0,0 +1,182 @@
+import {Parser} from '../parser';
+
+describe('when parsing', function() {
+
+  it('simple metric expression', function() {
+    var parser = new Parser('metric.test.*.asd.count');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('metric');
+    expect(rootNode.segments.length).toBe(5);
+    expect(rootNode.segments[0].value).toBe('metric');
+  });
+
+  it('simple metric expression with numbers in segments', function() {
+    var parser = new Parser('metric.10.15_20.5');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('metric');
+    expect(rootNode.segments.length).toBe(4);
+    expect(rootNode.segments[1].value).toBe('10');
+    expect(rootNode.segments[2].value).toBe('15_20');
+    expect(rootNode.segments[3].value).toBe('5');
+  });
+
+  it('simple metric expression with curly braces', function() {
+    var parser = new Parser('metric.se1-{count, max}');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('metric');
+    expect(rootNode.segments.length).toBe(2);
+    expect(rootNode.segments[1].value).toBe('se1-{count,max}');
+  });
+
+  it('simple metric expression with curly braces at start of segment and with post chars', function() {
+    var parser = new Parser('metric.{count, max}-something.count');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('metric');
+    expect(rootNode.segments.length).toBe(3);
+    expect(rootNode.segments[1].value).toBe('{count,max}-something');
+  });
+
+  it('simple function', function() {
+    var parser = new Parser('sum(test)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params.length).toBe(1);
+  });
+
+  it('simple function2', function() {
+    var parser = new Parser('offset(test.metric, -100)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params[0].type).toBe('metric');
+    expect(rootNode.params[1].type).toBe('number');
+  });
+
+  it('simple function with string arg', function() {
+    var parser = new Parser("randomWalk('test')");
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params.length).toBe(1);
+    expect(rootNode.params[0].type).toBe('string');
+  });
+
+  it('function with multiple args', function() {
+    var parser = new Parser("sum(test, 1, 'test')");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params.length).toBe(3);
+    expect(rootNode.params[0].type).toBe('metric');
+    expect(rootNode.params[1].type).toBe('number');
+    expect(rootNode.params[2].type).toBe('string');
+  });
+
+  it('function with nested function', function() {
+    var parser = new Parser("sum(scaleToSeconds(test, 1))");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params.length).toBe(1);
+    expect(rootNode.params[0].type).toBe('function');
+    expect(rootNode.params[0].name).toBe('scaleToSeconds');
+    expect(rootNode.params[0].params.length).toBe(2);
+    expect(rootNode.params[0].params[0].type).toBe('metric');
+    expect(rootNode.params[0].params[1].type).toBe('number');
+  });
+
+  it('function with multiple series', function() {
+    var parser = new Parser("sum(test.test.*.count, test.timers.*.count)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params.length).toBe(2);
+    expect(rootNode.params[0].type).toBe('metric');
+    expect(rootNode.params[1].type).toBe('metric');
+  });
+
+  it('function with templated series', function() {
+    var parser = new Parser("sum(test.[[server]].count)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).toBe(undefined);
+    expect(rootNode.params[0].type).toBe('metric');
+    expect(rootNode.params[0].segments[1].type).toBe('segment');
+    expect(rootNode.params[0].segments[1].value).toBe('[[server]]');
+  });
+
+  it('invalid metric expression', function() {
+    var parser = new Parser('metric.test.*.asd.');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).toBe('Expected metric identifier instead found end of string');
+    expect(rootNode.pos).toBe(19);
+  });
+
+  it('invalid function expression missing closing parenthesis', function() {
+    var parser = new Parser('sum(test');
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).toBe('Expected closing parenthesis instead found end of string');
+    expect(rootNode.pos).toBe(9);
+  });
+
+  it('unclosed string in function', function() {
+    var parser = new Parser("sum('test)");
+    var rootNode = parser.getAst();
+
+    expect(rootNode.message).toBe('Unclosed string parameter');
+    expect(rootNode.pos).toBe(11);
+  });
+
+  it('handle issue #69', function() {
+    var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+  });
+
+  it('handle float function arguments', function() {
+    var parser = new Parser('scale(test, 0.002)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params[1].type).toBe('number');
+    expect(rootNode.params[1].value).toBe(0.002);
+  });
+
+  it('handle curly brace pattern at start', function() {
+    var parser = new Parser('{apps}.test');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('metric');
+    expect(rootNode.segments[0].value).toBe('{apps}');
+    expect(rootNode.segments[1].value).toBe('test');
+  });
+
+  it('series parameters', function() {
+    var parser = new Parser('asPercent(#A, #B)');
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params[0].type).toBe('series-ref');
+    expect(rootNode.params[0].value).toBe('#A');
+    expect(rootNode.params[1].value).toBe('#B');
+  });
+
+  it('series parameters, issue 2788', function() {
+    var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
+    var rootNode = parser.getAst();
+    expect(rootNode.type).toBe('function');
+    expect(rootNode.params[0].type).toBe('function');
+    expect(rootNode.params[1].value).toBe('10m');
+    expect(rootNode.params[3].type).toBe('bool');
+  });
+
+  it('should parse metric expression with ip number segments', function() {
+    var parser = new Parser('5.10.123.5');
+    var rootNode = parser.getAst();
+    expect(rootNode.segments[0].value).toBe('5');
+    expect(rootNode.segments[1].value).toBe('10');
+    expect(rootNode.segments[2].value).toBe('123');
+    expect(rootNode.segments[3].value).toBe('5');
+  });
+});

+ 0 - 183
public/app/plugins/datasource/graphite/specs/parser_specs.ts

@@ -1,183 +0,0 @@
-import {describe, it, expect} from 'test/lib/common';
-import {Parser} from '../parser';
-
-describe('when parsing', function() {
-
-  it('simple metric expression', function() {
-    var parser = new Parser('metric.test.*.asd.count');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('metric');
-    expect(rootNode.segments.length).to.be(5);
-    expect(rootNode.segments[0].value).to.be('metric');
-  });
-
-  it('simple metric expression with numbers in segments', function() {
-    var parser = new Parser('metric.10.15_20.5');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('metric');
-    expect(rootNode.segments.length).to.be(4);
-    expect(rootNode.segments[1].value).to.be('10');
-    expect(rootNode.segments[2].value).to.be('15_20');
-    expect(rootNode.segments[3].value).to.be('5');
-  });
-
-  it('simple metric expression with curly braces', function() {
-    var parser = new Parser('metric.se1-{count, max}');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('metric');
-    expect(rootNode.segments.length).to.be(2);
-    expect(rootNode.segments[1].value).to.be('se1-{count,max}');
-  });
-
-  it('simple metric expression with curly braces at start of segment and with post chars', function() {
-    var parser = new Parser('metric.{count, max}-something.count');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('metric');
-    expect(rootNode.segments.length).to.be(3);
-    expect(rootNode.segments[1].value).to.be('{count,max}-something');
-  });
-
-  it('simple function', function() {
-    var parser = new Parser('sum(test)');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params.length).to.be(1);
-  });
-
-  it('simple function2', function() {
-    var parser = new Parser('offset(test.metric, -100)');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params[0].type).to.be('metric');
-    expect(rootNode.params[1].type).to.be('number');
-  });
-
-  it('simple function with string arg', function() {
-    var parser = new Parser("randomWalk('test')");
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params.length).to.be(1);
-    expect(rootNode.params[0].type).to.be('string');
-  });
-
-  it('function with multiple args', function() {
-    var parser = new Parser("sum(test, 1, 'test')");
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params.length).to.be(3);
-    expect(rootNode.params[0].type).to.be('metric');
-    expect(rootNode.params[1].type).to.be('number');
-    expect(rootNode.params[2].type).to.be('string');
-  });
-
-  it('function with nested function', function() {
-    var parser = new Parser("sum(scaleToSeconds(test, 1))");
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params.length).to.be(1);
-    expect(rootNode.params[0].type).to.be('function');
-    expect(rootNode.params[0].name).to.be('scaleToSeconds');
-    expect(rootNode.params[0].params.length).to.be(2);
-    expect(rootNode.params[0].params[0].type).to.be('metric');
-    expect(rootNode.params[0].params[1].type).to.be('number');
-  });
-
-  it('function with multiple series', function() {
-    var parser = new Parser("sum(test.test.*.count, test.timers.*.count)");
-    var rootNode = parser.getAst();
-
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params.length).to.be(2);
-    expect(rootNode.params[0].type).to.be('metric');
-    expect(rootNode.params[1].type).to.be('metric');
-  });
-
-  it('function with templated series', function() {
-    var parser = new Parser("sum(test.[[server]].count)");
-    var rootNode = parser.getAst();
-
-    expect(rootNode.message).to.be(undefined);
-    expect(rootNode.params[0].type).to.be('metric');
-    expect(rootNode.params[0].segments[1].type).to.be('segment');
-    expect(rootNode.params[0].segments[1].value).to.be('[[server]]');
-  });
-
-  it('invalid metric expression', function() {
-    var parser = new Parser('metric.test.*.asd.');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.message).to.be('Expected metric identifier instead found end of string');
-    expect(rootNode.pos).to.be(19);
-  });
-
-  it('invalid function expression missing closing parenthesis', function() {
-    var parser = new Parser('sum(test');
-    var rootNode = parser.getAst();
-
-    expect(rootNode.message).to.be('Expected closing parenthesis instead found end of string');
-    expect(rootNode.pos).to.be(9);
-  });
-
-  it('unclosed string in function', function() {
-    var parser = new Parser("sum('test)");
-    var rootNode = parser.getAst();
-
-    expect(rootNode.message).to.be('Unclosed string parameter');
-    expect(rootNode.pos).to.be(11);
-  });
-
-  it('handle issue #69', function() {
-    var parser = new Parser('cactiStyle(offset(scale(net.192-168-1-1.192-168-1-9.ping_value.*,0.001),-100))');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-  });
-
-  it('handle float function arguments', function() {
-    var parser = new Parser('scale(test, 0.002)');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params[1].type).to.be('number');
-    expect(rootNode.params[1].value).to.be(0.002);
-  });
-
-  it('handle curly brace pattern at start', function() {
-    var parser = new Parser('{apps}.test');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('metric');
-    expect(rootNode.segments[0].value).to.be('{apps}');
-    expect(rootNode.segments[1].value).to.be('test');
-  });
-
-  it('series parameters', function() {
-    var parser = new Parser('asPercent(#A, #B)');
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params[0].type).to.be('series-ref');
-    expect(rootNode.params[0].value).to.be('#A');
-    expect(rootNode.params[1].value).to.be('#B');
-  });
-
-  it('series parameters, issue 2788', function() {
-    var parser = new Parser("summarize(diffSeries(#A, #B), '10m', 'sum', false)");
-    var rootNode = parser.getAst();
-    expect(rootNode.type).to.be('function');
-    expect(rootNode.params[0].type).to.be('function');
-    expect(rootNode.params[1].value).to.be('10m');
-    expect(rootNode.params[3].type).to.be('bool');
-  });
-
-  it('should parse metric expression with ip number segments', function() {
-    var parser = new Parser('5.10.123.5');
-    var rootNode = parser.getAst();
-    expect(rootNode.segments[0].value).to.be('5');
-    expect(rootNode.segments[1].value).to.be('10');
-    expect(rootNode.segments[2].value).to.be('123');
-    expect(rootNode.segments[3].value).to.be('5');
-  });
-});

+ 4 - 0
public/app/plugins/datasource/postgres/datasource.ts

@@ -20,6 +20,10 @@ export class PostgresDatasource {
       return '\'' + value + '\'';
     }
 
+    if (typeof value === 'number') {
+      return value.toString();
+    }
+
     var quotedValues = _.map(value, function(val) {
       return '\'' + val + '\'';
     });

+ 2 - 2
public/app/plugins/datasource/postgres/module.ts

@@ -16,8 +16,8 @@ class PostgresConfigCtrl {
 
 const defaultQuery = `SELECT
   extract(epoch from time_column) AS time,
-  title_column as title,
-  description_column as text
+  text_column as text,
+  tags_column as tags
 FROM
   metric_table
 WHERE

+ 4 - 2
public/app/plugins/datasource/prometheus/completer.ts

@@ -8,7 +8,7 @@ export class PromCompleter {
   labelNameCache: any;
   labelValueCache: any;
 
-  identifierRegexps = [/[\[\]a-zA-Z_0-9=]/];
+  identifierRegexps = [/\[/, /[a-zA-Z0-9_:]/];
 
   constructor(private datasource: PrometheusDatasource) {
     this.labelQueryCache = {};
@@ -73,13 +73,15 @@ export class PromCompleter {
         });
     }
 
-    if (prefix === '[') {
+    if (token.type === 'paren.lparen' && token.value === '[') {
       var vectors = [];
       for (let unit of ['s', 'm', 'h']) {
         for (let value of [1,5,10,30]) {
          vectors.push({caption: value+unit, value: '['+value+unit, meta: 'range vector'});
         }
       }
+      vectors.push({caption: '$__interval', value: '[$__interval', meta: 'range vector'});
+      vectors.push({caption: '$__interval_ms', value: '[$__interval_ms', meta: 'range vector'});
       callback(null, vectors);
       return;
     }

+ 1 - 1
public/app/plugins/datasource/prometheus/datasource.ts

@@ -168,7 +168,7 @@ export class PrometheusDatasource {
     if (interval !== 0 && range / intervalFactor / interval > 11000) {
       interval = Math.ceil(range / intervalFactor / 11000);
     }
-    return Math.max(interval * intervalFactor, minInterval);
+    return Math.max(interval * intervalFactor, minInterval, 1);
   }
 
   performTimeSeriesQuery(query, start, end) {

+ 3 - 9
public/app/plugins/datasource/prometheus/mode-prometheus.js

@@ -43,10 +43,10 @@ var PrometheusHighlightRules = function() {
       regex : "\\d+[smhdwy]"
     }, {
       token : keywordMapper,
-      regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
+      regex : "[a-zA-Z_:][a-zA-Z0-9_:]*"
     }, {
       token : "keyword.operator",
-      regex : "\\+|\\-|\\*|\\/|%|\\^|=|==|!=|<=|>=|<|>|=\\~|!\\~"
+      regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>"
     }, {
       token : "paren.lparen",
       regex : "[[(]"
@@ -75,8 +75,7 @@ var PrometheusHighlightRules = function() {
       regex : '"[^"]*"|\'[^\']*\''
     }, {
       token : "punctuation.operator",
-      regex : ",",
-      push  : 'start-label-matcher'
+      regex : ","
     }, {
       token : "paren.rparen",
       regex : "}",
@@ -112,11 +111,6 @@ var keyWordsCompletions = prometheusKeyWords.map(function(word) {
 });
 
 var prometheusFunctions = [
-  {
-    name: 'abs()', value: 'abs',
-    def: 'abs(v instant-vector)',
-    docText: 'Returns the input vector with all sample values converted to their absolute value.'
-  },
   {
     name: 'abs()', value: 'abs',
     def: 'abs(v instant-vector)',

+ 11 - 5
public/app/plugins/datasource/prometheus/specs/completer_specs.ts

@@ -44,12 +44,18 @@ describe('Prometheus editor completer', function() {
   describe('When inside brackets', () => {
     it('Should return range vectors', () => {
       const session = getSessionStub({
-        currentToken: {},
-        tokens: [],
-        line: '',
+        currentToken: {type: 'paren.lparen', value: '[', index: 2, start: 9},
+        tokens: [
+          {type: 'identifier', value: 'node_cpu'},
+          {type: 'paren.lparen', value: '['}
+        ],
+        line: 'node_cpu[',
       });
-      completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => {
-        expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'});
+
+      return completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => {
+        expect(res[0].caption).to.eql('1s');
+        expect(res[0].value).to.eql('[1s');
+        expect(res[0].meta).to.eql('range vector');
       });
     });
   });

+ 14 - 0
public/app/plugins/datasource/prometheus/specs/datasource_specs.ts

@@ -294,6 +294,20 @@ describe('PrometheusDatasource', function() {
       ctx.ds.query(query);
       ctx.$httpBackend.verifyNoOutstandingExpectation();
     });
+
+    it('step should never go below 1', function() {
+      var query = {
+        // 6 hour range
+        range: { from: moment(1508318768202), to: moment(1508318770118) },
+        targets: [{expr: 'test'}],
+        interval: '100ms'
+      };
+      var urlExpected = 'proxied/api/v1/query_range?query=test&start=1508318769&end=1508318771&step=1';
+      ctx.$httpBackend.expect('GET', urlExpected).respond(response);
+      ctx.ds.query(query);
+      ctx.$httpBackend.verifyNoOutstandingExpectation();
+    });
+
     it('should be auto interval when greater than min interval', function() {
       var query = {
         // 6 hour range

+ 1 - 1
public/app/plugins/panel/gettingstarted/module.ts

@@ -34,7 +34,7 @@ class GettingStartedPanelCtrl extends PanelCtrl {
       check: () => {
         return $q.when(
           datasourceSrv.getMetricSources().filter(item => {
-            return item.meta.builtIn === false;
+            return item.meta.builtIn !== true;
           }).length > 0
         );
       }

+ 36 - 1
public/app/plugins/panel/graph/graph.ts

@@ -343,7 +343,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         eventManager.addFlotEvents(annotations, options);
         configureAxisOptions(data, options);
 
-        sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
+        sortedSeries = sortSeries(data, ctrl.panel);
 
         function callPlot(incrementRenderCounter) {
           try {
@@ -374,6 +374,41 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) {
         }
       }
 
+      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;
+
+        if (panel.stack && haveSortBy && haveSortOrder) {
+          var desc = desc = panel.legend.sortDesc === true ? 1 : -1;
+          series.sort((x, y) => {
+            if (x.stats[sortBy] > y.stats[sortBy]) {
+              return 1 * desc;
+            }
+            if (x.stats[sortBy] < y.stats[sortBy]) {
+              return -1 * desc;
+            }
+
+            return 0;
+          });
+        }
+
+        series.sort((x, y) => {
+          if (x.zindex > y.zindex) {
+            return 1;
+          }
+
+          if (x.zindex < y.zindex) {
+            return -1;
+          }
+
+          return 0;
+        });
+
+        return series;
+      }
+
       function translateFillOption(fill) {
         if (panel.percentage && panel.stack) {
           return fill === 0 ? 0.001 : fill/10;

+ 2 - 2
public/app/plugins/panel/graph/legend.js

@@ -80,13 +80,13 @@ function (angular, _, $) {
           if (panel.legend.sortDesc === false) {
             panel.legend.sort = null;
             panel.legend.sortDesc = null;
-            render();
+            ctrl.render();
             return;
           }
 
           panel.legend.sortDesc = !panel.legend.sortDesc;
           panel.legend.sort = stat;
-          render();
+          ctrl.render();
         }
 
         function getTableHeaderHtml(statName) {

+ 58 - 9
public/app/plugins/panel/graph/specs/graph_specs.ts

@@ -15,16 +15,16 @@ describe('grafanaGraph', function() {
   beforeEach(angularMocks.module('grafana.core'));
 
   function graphScenario(desc, func, elementWidth = 500)  {
-    describe(desc, function() {
+    describe(desc, () => {
       var ctx: any = {};
 
-      ctx.setup = function(setupFunc) {
+      ctx.setup = (setupFunc) => {
 
-        beforeEach(angularMocks.module(function($provide) {
+        beforeEach(angularMocks.module(($provide) => {
           $provide.value("timeSrv", new helpers.TimeSrvStub());
         }));
 
-        beforeEach(angularMocks.inject(function($rootScope, $compile) {
+        beforeEach(angularMocks.inject(($rootScope, $compile) => {
           var ctrl: any = {
             height: 200,
             panel: {
@@ -75,7 +75,7 @@ describe('grafanaGraph', function() {
             alias: 'series1'
           }));
           ctx.data.push(new TimeSeries({
-            datapoints: [[1,1],[2,2]],
+            datapoints: [[1,10],[2,20]],
             alias: 'series2'
           }));
 
@@ -96,15 +96,15 @@ describe('grafanaGraph', function() {
     });
   }
 
-  graphScenario('simple lines options', function(ctx) {
-    ctx.setup(function(ctrl) {
+  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', function() {
+    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);
@@ -112,6 +112,55 @@ describe('grafanaGraph', function() {
     });
   });
 
+  graphScenario('sort series as legend', (ctx) => {
+    describe("with sort as legend undefined", () => {
+      ctx.setup((ctrl) => {
+        ctrl.panel.legend.sort = undefined;
+      });
+
+      it("should not modify order of time series", () => {
+        expect(ctx.plotData[0].alias).to.be('series1');
+        expect(ctx.plotData[1].alias).to.be('series2');
+      });
+    });
+
+    describe("with sort as legend set to min. descending order", () => {
+      ctx.setup((ctrl) => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+      });
+
+      it("highest value should be first", () => {
+        expect(ctx.plotData[1].alias).to.be('series2');
+        expect(ctx.plotData[0].alias).to.be('series1');
+      });
+    });
+
+    describe("with sort as legend set to min. ascending order", () => {
+      ctx.setup((ctrl) => {
+        ctrl.panel.legend.sort = 'min';
+        ctrl.panel.legend.sortDesc = true;
+      });
+
+      it("lowest value should be first", () => {
+        expect(ctx.plotData[0].alias).to.be('series1');
+        expect(ctx.plotData[1].alias).to.be('series2');
+      });
+    });
+
+    describe("with sort as legend set to current. ascending order", () => {
+      ctx.setup((ctrl) => {
+        ctrl.panel.legend.sort = 'current';
+        ctrl.panel.legend.sortDesc = false;
+      });
+
+      it("highest last value should be first", () => {
+        expect(ctx.plotData[1].alias).to.be('series2');
+        expect(ctx.plotData[0].alias).to.be('series1');
+      });
+    });
+  });
+
   graphScenario('when logBase is log 10', function(ctx) {
     ctx.setup(function(ctrl, data) {
       ctrl.panel.yaxes[0].logBase = 10;
@@ -251,7 +300,7 @@ describe('grafanaGraph', function() {
     });
 
     it('should set barWidth', function() {
-      expect(ctx.plotOptions.series.bars.barWidth).to.be(1/1.5);
+      expect(ctx.plotOptions.series.bars.barWidth).to.be(10/1.5);
     });
   });
 

+ 11 - 2
public/app/plugins/panel/heatmap/color_legend.ts

@@ -152,7 +152,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
     .tickSize(2);
 
   let colorRect = legendElem.find(":first-child");
-  let posY = colorRect.height() + 2;
+  let posY = getSvgElemHeight(legendElem) + 2;
   let posX = getSvgElemX(colorRect);
 
   d3.select(legendElem.get(0)).append("g")
@@ -256,7 +256,16 @@ function getOpacityScale(options, maxValue, minValue = 0) {
 function getSvgElemX(elem) {
   let svgElem = elem.get(0);
   if (svgElem && svgElem.x && svgElem.x.baseVal) {
-    return elem.get(0).x.baseVal.value;
+    return svgElem.x.baseVal.value;
+  } else {
+    return 0;
+  }
+}
+
+function getSvgElemHeight(elem) {
+  let svgElem = elem.get(0);
+  if (svgElem && svgElem.height && svgElem.height.baseVal) {
+    return svgElem.height.baseVal.value;
   } else {
     return 0;
   }

+ 3 - 6
public/app/plugins/panel/heatmap/heatmap_ctrl.ts

@@ -1,13 +1,11 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import {MetricsPanelCtrl} from 'app/plugins/sdk';
 import _ from 'lodash';
 import kbn from 'app/core/utils/kbn';
-import TimeSeries from 'app/core/time_series';
+import TimeSeries from 'app/core/time_series2';
 import {axesEditor} from './axes_editor';
 import {heatmapDisplayEditor} from './display_editor';
 import rendering from './rendering';
-import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize, getMinLog} from './heatmap_data_converter';
+import {convertToHeatMap, convertToCards, elasticHistogramToHeatmap, calculateBucketSize } from './heatmap_data_converter';
 
 let X_BUCKET_NUMBER_DEFAULT = 30;
 let Y_BUCKET_NUMBER_DEFAULT = 10;
@@ -250,7 +248,6 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
     });
 
     series.flotpairs = series.getFlotPairs(this.panel.nullPointMode);
-    series.minLog = getMinLog(series);
 
     let datapoints = seriesData.datapoints || [];
     if (datapoints && datapoints.length > 0) {
@@ -266,7 +263,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl {
 
   parseSeries(series) {
     let min = _.min(_.map(series, s => s.stats.min));
-    let minLog = _.min(_.map(series, s => s.minLog));
+    let minLog = _.min(_.map(series, s => s.stats.logmin));
     let max = _.max(_.map(series, s => s.stats.max));
 
     return {

+ 0 - 9
public/app/plugins/panel/heatmap/heatmap_data_converter.ts

@@ -1,5 +1,3 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import _ from 'lodash';
 
 let VALUE_INDEX = 0;
@@ -320,12 +318,6 @@ function convertToLogScaleValueBuckets(xBucket, yBucketSplitFactor, logBase) {
   return buckets;
 }
 
-// Get minimum non zero value.
-function getMinLog(series) {
-  let values = _.compact(_.map(series.datapoints, p => p[0]));
-  return _.min(values);
-}
-
 /**
  * Logarithm for custom base
  * @param value
@@ -432,7 +424,6 @@ export {
   elasticHistogramToHeatmap,
   convertToCards,
   mergeZeroBuckets,
-  getMinLog,
   getValueBucketBound,
   isHeatmapDataEqual,
   calculateBucketSize

+ 2 - 3
public/app/plugins/panel/heatmap/rendering.ts

@@ -71,9 +71,8 @@ export default function link(scope, elem, attrs, ctrl) {
   function getYAxisWidth(elem) {
     let axis_text = elem.selectAll(".axis-y text").nodes();
     let max_text_width = _.max(_.map(axis_text, text => {
-      let el = $(text);
-      // Use JQuery outerWidth() to compute full element width
-      return el.outerWidth();
+      // Use SVG getBBox method
+      return text.getBBox().width;
     }));
 
     return max_text_width;

+ 19 - 21
public/app/plugins/panel/heatmap/specs/heatmap_data_converter_specs.ts → public/app/plugins/panel/heatmap/specs/heatmap_data_converter.jest.ts

@@ -1,5 +1,3 @@
-///<reference path="../../../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import { describe, beforeEach, it, expect } from '../../../../../test/lib/common';
 import TimeSeries from 'app/core/time_series2';
@@ -45,23 +43,23 @@ describe('isHeatmapDataEqual', () => {
     let emptyValues = _.cloneDeep(ctx.heatmapA);
     emptyValues['1422774000000'].buckets['1'].values = [];
 
-    expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).to.be(true);
-    expect(isHeatmapDataEqual(ctx.heatmapB, ctx.heatmapA)).to.be(true);
+    expect(isHeatmapDataEqual(ctx.heatmapA, ctx.heatmapB)).toBe(true);
+    expect(isHeatmapDataEqual(ctx.heatmapB, ctx.heatmapA)).toBe(true);
 
-    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapC)).to.be(true);
-    expect(isHeatmapDataEqual(heatmapC, ctx.heatmapA)).to.be(true);
+    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapC)).toBe(true);
+    expect(isHeatmapDataEqual(heatmapC, ctx.heatmapA)).toBe(true);
 
-    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapD)).to.be(false);
-    expect(isHeatmapDataEqual(heatmapD, ctx.heatmapA)).to.be(false);
+    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapD)).toBe(false);
+    expect(isHeatmapDataEqual(heatmapD, ctx.heatmapA)).toBe(false);
 
-    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapE)).to.be(false);
-    expect(isHeatmapDataEqual(heatmapE, ctx.heatmapA)).to.be(false);
+    expect(isHeatmapDataEqual(ctx.heatmapA, heatmapE)).toBe(false);
+    expect(isHeatmapDataEqual(heatmapE, ctx.heatmapA)).toBe(false);
 
-    expect(isHeatmapDataEqual(empty, ctx.heatmapA)).to.be(false);
-    expect(isHeatmapDataEqual(ctx.heatmapA, empty)).to.be(false);
+    expect(isHeatmapDataEqual(empty, ctx.heatmapA)).toBe(false);
+    expect(isHeatmapDataEqual(ctx.heatmapA, empty)).toBe(false);
 
-    expect(isHeatmapDataEqual(emptyValues, ctx.heatmapA)).to.be(false);
-    expect(isHeatmapDataEqual(ctx.heatmapA, emptyValues)).to.be(false);
+    expect(isHeatmapDataEqual(emptyValues, ctx.heatmapA)).toBe(false);
+    expect(isHeatmapDataEqual(ctx.heatmapA, emptyValues)).toBe(false);
   });
 });
 
@@ -87,7 +85,7 @@ describe('calculateBucketSize', () => {
     it('should properly calculate bucket size', () => {
       _.each(ctx.bounds_set, (b) => {
         let bucketSize = calculateBucketSize(b.bounds, ctx.logBase);
-        expect(bucketSize).to.be(b.size);
+        expect(bucketSize).toBe(b.size);
       });
     });
   });
@@ -108,7 +106,7 @@ describe('calculateBucketSize', () => {
     it('should properly calculate bucket size', () => {
       _.each(ctx.bounds_set, (b) => {
         let bucketSize = calculateBucketSize(b.bounds, ctx.logBase);
-        expect(isEqual(bucketSize, b.size)).to.be(true);
+        expect(isEqual(bucketSize, b.size)).toBe(true);
       });
     });
   });
@@ -162,7 +160,7 @@ describe('HeatmapDataConverter', () => {
       };
 
       let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase);
-      expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true);
+      expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true);
     });
   });
 
@@ -190,7 +188,7 @@ describe('HeatmapDataConverter', () => {
       };
 
       let heatmap = convertToHeatMap(ctx.series, ctx.yBucketSize, ctx.xBucketSize, ctx.logBase);
-      expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).to.be(true);
+      expect(isHeatmapDataEqual(heatmap, expectedHeatmap)).toBe(true);
     });
   });
 });
@@ -240,7 +238,7 @@ describe('ES Histogram converter', () => {
       };
 
       let heatmap = elasticHistogramToHeatmap(ctx.series);
-      expect(heatmap).to.eql(expectedHeatmap);
+      expect(heatmap).toMatchObject(expectedHeatmap);
     });
   });
 });
@@ -273,13 +271,13 @@ describe('convertToCards', () => {
       {x: 1422774060000, y: 2, count: 2, values: [2, 3], yBounds: {}}
     ];
     let res = convertToCards(buckets);
-    expect(res.cards).to.eql(expectedCards);
+    expect(res.cards).toMatchObject(expectedCards);
   });
 
   it('should build proper cards stats', () => {
     let expectedStats = {min: 1, max: 2};
     let res = convertToCards(buckets);
-    expect(res.cardStats).to.eql(expectedStats);
+    expect(res.cardStats).toMatchObject(expectedStats);
   });
 });
 

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

@@ -1,5 +1,3 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import moment from 'moment';
 import kbn from 'app/core/utils/kbn';

+ 18 - 20
public/app/plugins/panel/table/specs/renderer_specs.ts → public/app/plugins/panel/table/specs/renderer.jest.ts

@@ -1,5 +1,3 @@
-import {describe, it, expect} from 'test/lib/common';
-
 import _ from 'lodash';
 import TableModel from 'app/core/table_model';
 import {TableRenderer} from '../renderer';
@@ -92,84 +90,84 @@ describe('when rendering table', () => {
 
     it('time column should be formated', () => {
       var html = renderer.renderCell(0, 0, 1388556366666);
-      expect(html).to.be('<td>2014-01-01T06:06:06Z</td>');
+      expect(html).toBe('<td>2014-01-01T06:06:06Z</td>');
     });
 
     it('undefined time column should be rendered as -', () => {
       var html = renderer.renderCell(0, 0, undefined);
-      expect(html).to.be('<td>-</td>');
+      expect(html).toBe('<td>-</td>');
     });
 
     it('null time column should be rendered as -', () => {
       var html = renderer.renderCell(0, 0, null);
-      expect(html).to.be('<td>-</td>');
+      expect(html).toBe('<td>-</td>');
     });
 
     it('number column with unit specified should ignore style unit', () => {
       var html = renderer.renderCell(5, 0, 1230);
-      expect(html).to.be('<td>1.23 kbps</td>');
+      expect(html).toBe('<td>1.23 kbps</td>');
     });
 
     it('number column should be formated', () => {
       var html = renderer.renderCell(1, 0, 1230);
-      expect(html).to.be('<td>1.230 s</td>');
+      expect(html).toBe('<td>1.230 s</td>');
     });
 
     it('number style should ignore string values', () => {
       var html = renderer.renderCell(1, 0, 'asd');
-      expect(html).to.be('<td>asd</td>');
+      expect(html).toBe('<td>asd</td>');
     });
 
     it('colored cell should have style', () => {
       var html = renderer.renderCell(2, 0, 40);
-      expect(html).to.be('<td style="color:green">40.0</td>');
+      expect(html).toBe('<td style="color:green">40.0</td>');
     });
 
     it('colored cell should have style', () => {
       var html = renderer.renderCell(2, 0, 55);
-      expect(html).to.be('<td style="color:orange">55.0</td>');
+      expect(html).toBe('<td style="color:orange">55.0</td>');
     });
 
     it('colored cell should have style', () => {
       var html = renderer.renderCell(2, 0, 85);
-      expect(html).to.be('<td style="color:red">85.0</td>');
+      expect(html).toBe('<td style="color:red">85.0</td>');
     });
 
     it('unformated undefined should be rendered as string', () => {
       var html = renderer.renderCell(3, 0, 'value');
-      expect(html).to.be('<td>value</td>');
+      expect(html).toBe('<td>value</td>');
     });
 
     it('string style with escape html should return escaped html', () => {
       var html = renderer.renderCell(4, 0, "&breaking <br /> the <br /> row");
-      expect(html).to.be('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
+      expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
     });
 
     it('undefined formater should return escaped html', () => {
       var html = renderer.renderCell(3, 0, "&breaking <br /> the <br /> row");
-      expect(html).to.be('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
+      expect(html).toBe('<td>&amp;breaking &lt;br /&gt; the &lt;br /&gt; row</td>');
     });
 
     it('undefined value should render as -', () => {
       var html = renderer.renderCell(3, 0, undefined);
-      expect(html).to.be('<td></td>');
+      expect(html).toBe('<td></td>');
     });
 
     it('sanitized value should render as', () => {
       var html = renderer.renderCell(6, 0, 'text <a href="http://google.com">link</a>');
-      expect(html).to.be('<td>sanitized</td>');
+      expect(html).toBe('<td>sanitized</td>');
     });
 
     it('Time column title should be Timestamp', () => {
-      expect(table.columns[0].title).to.be('Timestamp');
+      expect(table.columns[0].title).toBe('Timestamp');
     });
 
     it('Value column title should be Val', () => {
-      expect(table.columns[1].title).to.be('Val');
+      expect(table.columns[1].title).toBe('Val');
     });
 
     it('Colored column title should be Colored', () => {
-      expect(table.columns[2].title).to.be('Colored');
+      expect(table.columns[2].title).toBe('Colored');
     });
 
     it('link should render as', () => {
@@ -182,7 +180,7 @@ describe('when rendering table', () => {
           </a>
         </td>
       `;
-      expect(normalize(html)).to.be(normalize(expectedHtml));
+      expect(normalize(html)).toBe(normalize(expectedHtml));
     });
   });
 });

+ 43 - 45
public/app/plugins/panel/table/specs/transformers_specs.ts → public/app/plugins/panel/table/specs/transformers.jest.ts

@@ -1,5 +1,3 @@
-import {describe, beforeEach, it, expect} from 'test/lib/common';
-
 import {transformers, transformDataToTable} from '../transformers';
 
 describe('when transforming time series table', () => {
@@ -29,18 +27,18 @@ describe('when transforming time series table', () => {
       });
 
       it('should return 3 rows', () => {
-        expect(table.rows.length).to.be(3);
-        expect(table.rows[0][1]).to.be('series1');
-        expect(table.rows[1][1]).to.be('series1');
-        expect(table.rows[2][1]).to.be('series2');
-        expect(table.rows[0][2]).to.be(12.12);
+        expect(table.rows.length).toBe(3);
+        expect(table.rows[0][1]).toBe('series1');
+        expect(table.rows[1][1]).toBe('series1');
+        expect(table.rows[2][1]).toBe('series2');
+        expect(table.rows[0][2]).toBe(12.12);
       });
 
       it('should return 3 rows', () => {
-        expect(table.columns.length).to.be(3);
-        expect(table.columns[0].text).to.be('Time');
-        expect(table.columns[1].text).to.be('Metric');
-        expect(table.columns[2].text).to.be('Value');
+        expect(table.columns.length).toBe(3);
+        expect(table.columns[0].text).toBe('Time');
+        expect(table.columns[1].text).toBe('Metric');
+        expect(table.columns[2].text).toBe('Value');
       });
     });
 
@@ -54,20 +52,20 @@ describe('when transforming time series table', () => {
       });
 
       it ('should return 3 columns', () => {
-        expect(table.columns.length).to.be(3);
-        expect(table.columns[0].text).to.be('Time');
-        expect(table.columns[1].text).to.be('series1');
-        expect(table.columns[2].text).to.be('series2');
+        expect(table.columns.length).toBe(3);
+        expect(table.columns[0].text).toBe('Time');
+        expect(table.columns[1].text).toBe('series1');
+        expect(table.columns[2].text).toBe('series2');
       });
 
       it ('should return 2 rows', () => {
-        expect(table.rows.length).to.be(2);
-        expect(table.rows[0][1]).to.be(12.12);
-        expect(table.rows[0][2]).to.be(16.12);
+        expect(table.rows.length).toBe(2);
+        expect(table.rows[0][1]).toBe(12.12);
+        expect(table.rows[0][2]).toBe(16.12);
       });
 
       it ('should be undefined when no value for timestamp', () => {
-        expect(table.rows[1][2]).to.be(undefined);
+        expect(table.rows[1][2]).toBe(undefined);
       });
     });
 
@@ -83,17 +81,17 @@ describe('when transforming time series table', () => {
       });
 
       it('should return 2 rows', () => {
-        expect(table.rows.length).to.be(2);
-        expect(table.rows[0][0]).to.be('series1');
-        expect(table.rows[0][1]).to.be(14.44);
-        expect(table.rows[0][2]).to.be(12.12);
+        expect(table.rows.length).toBe(2);
+        expect(table.rows[0][0]).toBe('series1');
+        expect(table.rows[0][1]).toBe(14.44);
+        expect(table.rows[0][2]).toBe(12.12);
       });
 
       it('should return 2 columns', () => {
-        expect(table.columns.length).to.be(3);
-        expect(table.columns[0].text).to.be('Metric');
-        expect(table.columns[1].text).to.be('Max');
-        expect(table.columns[2].text).to.be('Min');
+        expect(table.columns.length).toBe(3);
+        expect(table.columns[0].text).toBe('Metric');
+        expect(table.columns[1].text).toBe('Max');
+        expect(table.columns[2].text).toBe('Min');
       });
     });
 
@@ -124,9 +122,9 @@ describe('when transforming time series table', () => {
       describe('getColumns', function() {
         it('should return nested properties', function() {
           var columns = transformers['json'].getColumns(rawData);
-          expect(columns[0].text).to.be('timestamp');
-          expect(columns[1].text).to.be('message');
-          expect(columns[2].text).to.be('nested.level2');
+          expect(columns[0].text).toBe('timestamp');
+          expect(columns[1].text).toBe('message');
+          expect(columns[2].text).toBe('nested.level2');
         });
       });
 
@@ -136,17 +134,17 @@ describe('when transforming time series table', () => {
         });
 
         it ('should return 2 columns', () => {
-          expect(table.columns.length).to.be(3);
-          expect(table.columns[0].text).to.be('Timestamp');
-          expect(table.columns[1].text).to.be('Message');
-          expect(table.columns[2].text).to.be('nested.level2');
+          expect(table.columns.length).toBe(3);
+          expect(table.columns[0].text).toBe('Timestamp');
+          expect(table.columns[1].text).toBe('Message');
+          expect(table.columns[2].text).toBe('nested.level2');
         });
 
         it ('should return 2 rows', () => {
-          expect(table.rows.length).to.be(1);
-          expect(table.rows[0][0]).to.be('time');
-          expect(table.rows[0][1]).to.be('message');
-          expect(table.rows[0][2]).to.be('level2-value');
+          expect(table.rows.length).toBe(1);
+          expect(table.rows[0][0]).toBe('time');
+          expect(table.rows[0][1]).toBe('message');
+          expect(table.rows[0][2]).toBe('level2-value');
         });
       });
     });
@@ -169,16 +167,16 @@ describe('when transforming time series table', () => {
       });
 
       it ('should return 4 columns', () => {
-        expect(table.columns.length).to.be(4);
-        expect(table.columns[0].text).to.be('Time');
-        expect(table.columns[1].text).to.be('Title');
-        expect(table.columns[2].text).to.be('Text');
-        expect(table.columns[3].text).to.be('Tags');
+        expect(table.columns.length).toBe(4);
+        expect(table.columns[0].text).toBe('Time');
+        expect(table.columns[1].text).toBe('Title');
+        expect(table.columns[2].text).toBe('Text');
+        expect(table.columns[3].text).toBe('Tags');
       });
 
       it ('should return 1 rows', () => {
-        expect(table.rows.length).to.be(1);
-        expect(table.rows[0][0]).to.be(1000);
+        expect(table.rows.length).toBe(1);
+        expect(table.rows[0][0]).toBe(1000);
       });
     });
 

+ 0 - 2
public/app/plugins/panel/table/transformers.ts

@@ -1,5 +1,3 @@
-///<reference path="../../../headers/common.d.ts" />
-
 import _ from 'lodash';
 import flatten from '../../../core/utils/flatten';
 import TimeSeries from '../../../core/time_series2';

+ 0 - 339
public/test/core/utils/kbn_specs.js

@@ -1,339 +0,0 @@
-define([
-  'app/core/utils/kbn',
-  'app/core/utils/datemath',
-  'moment'
-], function(kbn, dateMath, moment) {
-  'use strict';
-
-  describe('unit format menu', function() {
-    var menu = kbn.getUnitFormats();
-    menu.map(function(submenu) {
-      describe('submenu ' + submenu.text, function() {
-        it('should have a title', function() { expect(submenu.text).to.be.a('string'); });
-        it('should have a submenu', function() { expect(submenu.submenu).to.be.an('array'); });
-        submenu.submenu.map(function(entry) {
-          describe('entry ' + entry.text, function() {
-            it('should have a title', function() { expect(entry.text).to.be.a('string'); });
-            it('should have a format', function() { expect(entry.value).to.be.a('string'); });
-            it('should have a valid format', function() {
-              expect(kbn.valueFormats[entry.value]).to.be.a('function');
-            });
-          });
-        });
-      });
-    });
-  });
-
-  function describeValueFormat(desc, value, tickSize, tickDecimals, result) {
-
-    describe('value format: ' + desc, function() {
-      it('should translate ' + value + ' as ' + result, function() {
-        var scaledDecimals = tickDecimals - Math.floor(Math.log(tickSize) / Math.LN10);
-        var str = kbn.valueFormats[desc](value, tickDecimals, scaledDecimals);
-        expect(str).to.be(result);
-      });
-    });
-
-  }
-
-  describeValueFormat('ms', 0.0024, 0.0005, 4, '0.0024 ms');
-  describeValueFormat('ms', 100, 1, 0, '100 ms');
-  describeValueFormat('ms', 1250, 10, 0, '1.25 s');
-  describeValueFormat('ms', 1250, 300, 0, '1.3 s');
-  describeValueFormat('ms', 65150, 10000, 0, '1.1 min');
-  describeValueFormat('ms', 6515000, 1500000, 0, '1.8 hour');
-  describeValueFormat('ms', 651500000, 150000000, 0, '8 day');
-
-  describeValueFormat('none', 2.75e-10, 0, 10, '3e-10');
-  describeValueFormat('none', 0, 0, 2, '0');
-  describeValueFormat('dB', 10, 1000, 2, '10.00 dB');
-
-  describeValueFormat('percent',  0, 0, 0, '0%');
-  describeValueFormat('percent', 53, 0, 1, '53.0%');
-  describeValueFormat('percentunit', 0.0, 0, 0, '0%');
-  describeValueFormat('percentunit', 0.278, 0, 1, '27.8%');
-  describeValueFormat('percentunit', 1.0, 0, 0, '100%');
-
-  describeValueFormat('currencyUSD', 7.42, 10000, 2, '$7.42');
-  describeValueFormat('currencyUSD', 1532.82, 1000, 1, '$1.53K');
-  describeValueFormat('currencyUSD', 18520408.7, 10000000, 0, '$19M');
-
-  describeValueFormat('bytes', -1.57e+308, -1.57e+308, 2, 'NA');
-
-  describeValueFormat('ns', 25, 1, 0, '25 ns');
-  describeValueFormat('ns', 2558, 50, 0, '2.56 µs');
-
-  describeValueFormat('ops', 123, 1, 0, '123 ops');
-  describeValueFormat('rps', 456000, 1000, -1, '456K rps');
-  describeValueFormat('rps', 123456789, 1000000, 2, '123.457M rps');
-  describeValueFormat('wps', 789000000, 1000000, -1, '789M wps');
-  describeValueFormat('iops', 11000000000, 1000000000, -1, '11B iops');
-
-  describeValueFormat('s', 1.23456789e-7, 1e-10, 8, '123.5 ns');
-  describeValueFormat('s', 1.23456789e-4, 1e-7, 5, '123.5 µs');
-  describeValueFormat('s', 1.23456789e-3, 1e-6, 4, '1.235 ms');
-  describeValueFormat('s', 1.23456789e-2, 1e-5, 3, '12.35 ms');
-  describeValueFormat('s', 1.23456789e-1, 1e-4, 2, '123.5 ms');
-  describeValueFormat('s', 24, 1, 0, '24 s');
-  describeValueFormat('s', 246, 1, 0, '4.1 min');
-  describeValueFormat('s', 24567, 100, 0, '6.82 hour');
-  describeValueFormat('s', 24567890, 10000, 0, '40.62 week');
-  describeValueFormat('s', 24567890000, 1000000, 0, '778.53 year');
-
-  describeValueFormat('m', 24, 1, 0, '24 min');
-  describeValueFormat('m', 246, 10, 0, '4.1 hour');
-  describeValueFormat('m', 6545, 10, 0, '4.55 day');
-  describeValueFormat('m', 24567, 100, 0, '2.44 week');
-  describeValueFormat('m', 24567892, 10000, 0, '46.7 year');
-
-  describeValueFormat('h', 21, 1, 0, '21 hour');
-  describeValueFormat('h', 145, 1, 0, '6.04 day');
-  describeValueFormat('h', 1234, 100, 0, '7.3 week');
-  describeValueFormat('h', 9458, 1000, 0, '1.08 year');
-
-  describeValueFormat('d', 3, 1, 0, '3 day');
-  describeValueFormat('d', 245, 100, 0, '35 week');
-  describeValueFormat('d', 2456, 10, 0, '6.73 year');
-
-  describe('date time formats', function() {
-    it('should format as iso date', function() {
-      var str = kbn.valueFormats.dateTimeAsIso(1505634997920, 1);
-      expect(str).to.be(moment(1505634997920).format('YYYY-MM-DD HH:mm:ss'));
-    });
-
-    it('should format as iso date and skip date when today', function() {
-      var now = moment();
-      var str = kbn.valueFormats.dateTimeAsIso(now.valueOf(), 1);
-      expect(str).to.be(now.format("HH:mm:ss"));
-    });
-
-    it('should format as US date', function() {
-      var str = kbn.valueFormats.dateTimeAsUS(1505634997920, 1);
-      expect(str).to.be(moment(1505634997920).format('MM/DD/YYYY H:mm:ss a'));
-    });
-
-    it('should format as US date and skip date when today', function() {
-      var now = moment();
-      var str = kbn.valueFormats.dateTimeAsUS(now.valueOf(), 1);
-      expect(str).to.be(now.format("h:mm:ss a"));
-    });
-
-    it('should format as from now with days', function() {
-      var daysAgo = moment().add(-7, 'd');
-      var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1);
-      expect(str).to.be('7 days ago');
-    });
-
-    it('should format as from now with minutes', function() {
-      var daysAgo = moment().add(-2, 'm');
-      var str = kbn.valueFormats.dateTimeFromNow(daysAgo.valueOf(), 1);
-      expect(str).to.be('2 minutes ago');
-    });
-  });
-
-  describe('kbn.toFixed and negative decimals', function() {
-    it('should treat as zero decimals', function() {
-      var str = kbn.toFixed(186.123, -2);
-      expect(str).to.be('186');
-    });
-  });
-
-  describe('kbn ms format when scaled decimals is null do not use it', function() {
-    it('should use specified decimals', function() {
-      var str = kbn.valueFormats['ms'](10000086.123, 1, null);
-      expect(str).to.be('2.8 hour');
-    });
-  });
-
-  describe('kbn kbytes format when scaled decimals is null do not use it', function() {
-    it('should use specified decimals', function() {
-      var str = kbn.valueFormats['kbytes'](10000000, 3, null);
-      expect(str).to.be('9.537 GiB');
-    });
-  });
-
-  describe('kbn deckbytes format when scaled decimals is null do not use it', function() {
-    it('should use specified decimals', function() {
-      var str = kbn.valueFormats['deckbytes'](10000000, 3, null);
-      expect(str).to.be('10.000 GB');
-    });
-  });
-
-  describe('kbn roundValue', function() {
-    it('should should handle null value', function() {
-      var str = kbn.roundValue(null, 2);
-      expect(str).to.be(null);
-    });
-  });
-
-  describe('calculateInterval', function() {
-    it('1h 100 resultion', function() {
-      var range = { from: dateMath.parse('now-1h'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 100, null);
-      expect(res.interval).to.be('30s');
-    });
-
-    it('10m 1600 resolution', function() {
-      var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 1600, null);
-      expect(res.interval).to.be('500ms');
-      expect(res.intervalMs).to.be(500);
-    });
-
-    it('fixed user min interval', function() {
-      var range = {from: dateMath.parse('now-10m'), to: dateMath.parse('now')};
-      var res = kbn.calculateInterval(range, 1600, '10s');
-      expect(res.interval).to.be('10s');
-      expect(res.intervalMs).to.be(10000);
-    });
-
-    it('short time range and user low limit', function() {
-      var range = { from: dateMath.parse('now-10m'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 1600, '>10s');
-      expect(res.interval).to.be('10s');
-    });
-
-    it('large time range and user low limit', function() {
-      var range = {from: dateMath.parse('now-14d'), to: dateMath.parse('now')};
-      var res = kbn.calculateInterval(range, 1000, '>10s');
-      expect(res.interval).to.be('20m');
-    });
-
-    it('10s 900 resolution and user low limit in ms', function() {
-      var range = { from: dateMath.parse('now-10s'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 900, '>15ms');
-      expect(res.interval).to.be('15ms');
-    });
-
-    it('1d 1 resolution', function() {
-      var range = { from: dateMath.parse('now-1d'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 1, null);
-      expect(res.interval).to.be('1d');
-      expect(res.intervalMs).to.be(86400000);
-    });
-
-    it('86399s 1 resolution', function() {
-      var range = { from: dateMath.parse('now-86390s'), to: dateMath.parse('now') };
-      var res = kbn.calculateInterval(range, 1, null);
-      expect(res.interval).to.be('12h');
-      expect(res.intervalMs).to.be(43200000);
-    });
-  });
-
-  describe('hex', function() {
-    it('positive integer', function() {
-      var str = kbn.valueFormats.hex(100, 0);
-      expect(str).to.be('64');
-    });
-    it('negative integer', function() {
-      var str = kbn.valueFormats.hex(-100, 0);
-      expect(str).to.be('-64');
-    });
-    it('null', function() {
-      var str = kbn.valueFormats.hex(null, 0);
-      expect(str).to.be('');
-    });
-    it('positive float', function() {
-      var str = kbn.valueFormats.hex(50.52, 1);
-      expect(str).to.be('32.8');
-    });
-    it('negative float', function() {
-      var str = kbn.valueFormats.hex(-50.333, 2);
-      expect(str).to.be('-32.547AE147AE14');
-    });
-  });
-
-  describe('hex 0x', function() {
-    it('positive integeter', function() {
-      var str = kbn.valueFormats.hex0x(7999,0);
-      expect(str).to.be('0x1F3F');
-    });
-    it('negative integer', function() {
-      var str = kbn.valueFormats.hex0x(-584,0);
-      expect(str).to.be('-0x248');
-    });
-    it('null', function() {
-      var str = kbn.valueFormats.hex0x(null, 0);
-      expect(str).to.be('');
-    });
-    it('positive float', function() {
-      var str = kbn.valueFormats.hex0x(74.443, 3);
-      expect(str).to.be('0x4A.716872B020C4');
-    });
-    it('negative float', function() {
-      var str = kbn.valueFormats.hex0x(-65.458, 1);
-      expect(str).to.be('-0x41.8');
-    });
-  });
-
-  describe('duration', function() {
-    it('null', function() {
-      var str = kbn.toDuration(null, 0, "millisecond");
-      expect(str).to.be('');
-    });
-    it('0 milliseconds', function() {
-      var str = kbn.toDuration(0, 0, "millisecond");
-      expect(str).to.be('0 milliseconds');
-    });
-    it('1 millisecond', function() {
-      var str = kbn.toDuration(1, 0, "millisecond");
-      expect(str).to.be('1 millisecond');
-    });
-    it('-1 millisecond', function() {
-      var str = kbn.toDuration(-1, 0, "millisecond");
-      expect(str).to.be('1 millisecond ago');
-    });
-    it('seconds', function() {
-      var str = kbn.toDuration(1, 0, "second");
-      expect(str).to.be('1 second');
-    });
-    it('minutes', function() {
-      var str = kbn.toDuration(1, 0, "minute");
-      expect(str).to.be('1 minute');
-    });
-    it('hours', function() {
-      var str = kbn.toDuration(1, 0, "hour");
-      expect(str).to.be('1 hour');
-    });
-    it('days', function() {
-      var str = kbn.toDuration(1, 0, "day");
-      expect(str).to.be('1 day');
-    });
-    it('weeks', function() {
-      var str = kbn.toDuration(1, 0, "week");
-      expect(str).to.be('1 week');
-    });
-    it('months', function() {
-      var str = kbn.toDuration(1, 0, "month");
-      expect(str).to.be('1 month');
-    });
-    it('years', function() {
-      var str = kbn.toDuration(1, 0, "year");
-      expect(str).to.be('1 year');
-    });
-    it('decimal days', function() {
-      var str = kbn.toDuration(1.5, 2, "day");
-      expect(str).to.be('1 day, 12 hours, 0 minutes');
-    });
-    it('decimal months', function() {
-      var str = kbn.toDuration(1.5, 3, "month");
-      expect(str).to.be('1 month, 2 weeks, 1 day, 0 hours');
-    });
-    it('no decimals', function() {
-      var str = kbn.toDuration(38898367008, 0, "millisecond");
-      expect(str).to.be('1 year');
-    });
-    it('1 decimal', function() {
-      var str = kbn.toDuration(38898367008, 1, "millisecond");
-      expect(str).to.be('1 year, 2 months');
-    });
-    it('too many decimals', function() {
-      var str = kbn.toDuration(38898367008, 20, "millisecond");
-      expect(str).to.be('1 year, 2 months, 3 weeks, 4 days, 5 hours, 6 minutes, 7 seconds, 8 milliseconds');
-    });
-    it('floating point error', function() {
-      var str = kbn.toDuration(36993906007, 8, "millisecond");
-      expect(str).to.be('1 year, 2 months, 0 weeks, 3 days, 4 hours, 5 minutes, 6 seconds, 7 milliseconds');
-    });
-  });
-});

+ 4 - 0
public/test/jest-setup.ts

@@ -0,0 +1,4 @@
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+
+configure({ adapter: new Adapter() });

+ 6 - 0
public/test/jest-shim.ts

@@ -0,0 +1,6 @@
+declare var global: NodeJS.Global;
+
+(<any>global).requestAnimationFrame = (callback) => {
+  setTimeout(callback, 0);
+};
+

+ 24 - 3
scripts/circle-test.sh

@@ -16,8 +16,16 @@ rm -rf node_modules
 npm install -g yarn --quiet
 yarn install --pure-lockfile --no-progress
 
-exit_if_fail npm test
-exit_if_fail npm build
+exit_if_fail npm run test-ci
+exit_if_fail npm run build
+
+# publish code coverage
+echo "Publishing javascript code coverage"
+bash <(curl -s https://codecov.io/bash) -cF javascript
+rm -rf coverage
+# npm install -g codecov
+# codecov
+# cat ./coverage/lcov.info | node ./node_modules/coveralls/bin/coveralls.js
 
 echo "running go fmt"
 exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)"
@@ -29,4 +37,17 @@ echo "building binaries"
 exit_if_fail go run build.go build
 
 echo "running go test"
-exit_if_fail go test -v ./pkg/...
+
+set -e
+echo "" > coverage.txt
+
+for d in $(go list ./pkg/...); do
+  exit_if_fail go test -race -coverprofile=profile.out -covermode=atomic $d
+  if [ -f profile.out ]; then
+    cat profile.out >> coverage.txt
+    rm profile.out
+  fi
+done
+
+echo "Publishing go code coverage"
+bash <(curl -s https://codecov.io/bash) -cF go

+ 1 - 0
scripts/grunt/default_task.js

@@ -14,6 +14,7 @@ module.exports = function(grunt) {
     'jshint',
     'sasslint',
     'exec:tslint',
+    "exec:jest",
     'karma:test',
     'no-only-tests'
   ]);

+ 8 - 1
scripts/grunt/options/exec.js

@@ -1,6 +1,13 @@
 module.exports = function(config, grunt) {
   'use strict'
+
+  var coverage = '';
+  if (config.coverage) {
+    coverage = '--coverage';
+  }
+
   return {
-    tslint : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json --type-check",
+    tslint : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json",
+    jest : "node ./node_modules/jest-cli/bin/jest.js " + coverage,
   };
 };

+ 4 - 0
scripts/webpack/webpack.prod.js

@@ -26,6 +26,10 @@ module.exports = merge(common, {
     ]
   },
 
+  devServer: {
+    stats: 'errors-only',
+  },
+
   plugins: [
     new ExtractTextPlugin({
       filename: 'grafana.[name].css',

+ 2 - 2
tslint.json

@@ -2,7 +2,8 @@
   "rules": {
     "no-string-throw": true,
     "no-unused-expression": true,
-		"no-unused-variable": true,
+		"no-unused-variable": false,
+    "no-use-before-declare": false,
     "no-duplicate-variable": true,
     "curly": true,
     "class-name": true,
@@ -33,7 +34,6 @@
     "no-string-literal": false,
     "no-switch-case-fall-through": false,
     "no-trailing-whitespace": true,
-    "no-use-before-declare": true,
     "no-var-keyword": false,
     "object-literal-sort-keys": false,
     "one-line": [true,

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 505 - 69
yarn.lock


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác