소스 검색

Merge remote-tracking branch 'upstream/master' into postgres-query-builder

Sven Klemm 7 년 전
부모
커밋
1d711924bc

+ 3 - 0
.circleci/config.yml

@@ -88,6 +88,9 @@ jobs:
       - run:
           name: run linters
           command: 'gometalinter.v2 --enable-gc --vendor --deadline 10m --disable-all --enable=deadcode --enable=ineffassign --enable=structcheck --enable=unconvert --enable=varcheck ./...'
+      - run:
+          name: run go vet 
+          command: 'go vet ./pkg/...'
 
   test-frontend:
     docker:

+ 1 - 0
CHANGELOG.md

@@ -15,6 +15,7 @@
 * **Postgres/MySQL/MSSQL**: Use floor rounding in $__timeGroup macro function [#12460](https://github.com/grafana/grafana/issues/12460), thx [@svenklemm](https://github.com/svenklemm)
 * **MySQL/MSSQL**: Use datetime format instead of epoch for $__timeFilter, $__timeFrom and $__timeTo macros [#11618](https://github.com/grafana/grafana/issues/11618) [#11619](https://github.com/grafana/grafana/issues/11619), thx [@AustinWinstanley](https://github.com/AustinWinstanley)
 * **Github OAuth**: Allow changes of user info at Github to be synched to Grafana when signing in [#11818](https://github.com/grafana/grafana/issues/11818), thx [@rwaweber](https://github.com/rwaweber)
+* **Alerting**: Fix diff and percent_diff reducers [#11563](https://github.com/grafana/grafana/issues/11563), thx [@jessetane](https://github.com/jessetane)
 
 # 5.2.2 (unreleased)
 

+ 3 - 3
package.json

@@ -149,17 +149,17 @@
     "classnames": "^2.2.5",
     "clipboard": "^1.7.1",
     "d3": "^4.11.0",
-    "d3-scale-chromatic": "^1.1.1",
+    "d3-scale-chromatic": "^1.3.0",
     "eventemitter3": "^2.0.3",
     "file-saver": "^1.3.3",
     "immutable": "^3.8.2",
     "jquery": "^3.2.1",
-    "lodash": "^4.17.4",
+    "lodash": "^4.17.10",
     "mini-css-extract-plugin": "^0.4.0",
     "mobx": "^3.4.1",
     "mobx-react": "^4.3.5",
     "mobx-state-tree": "^1.3.1",
-    "moment": "^2.18.1",
+    "moment": "^2.22.2",
     "mousetrap": "^1.6.0",
     "mousetrap-global-bind": "^1.1.0",
     "optimize-css-assets-webpack-plugin": "^4.0.2",

+ 4 - 4
pkg/services/alerting/conditions/reducer.go

@@ -108,9 +108,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
 				break
 			}
 		}
-		// get other points
+		// get the oldest point
 		points = points[0:i]
-		for i := len(points) - 1; i >= 0; i-- {
+		for i := 0; i < len(points); i++ {
 			if points[i][0].Valid {
 				allNull = false
 				value = first - points[i][0].Float64
@@ -131,9 +131,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
 				break
 			}
 		}
-		// get other points
+		// get the oldest point
 		points = points[0:i]
-		for i := len(points) - 1; i >= 0; i-- {
+		for i := 0; i < len(points); i++ {
 			if points[i][0].Valid {
 				allNull = false
 				val := (first - points[i][0].Float64) / points[i][0].Float64 * 100

+ 21 - 2
pkg/services/alerting/conditions/reducer_test.go

@@ -110,16 +110,35 @@ func TestSimpleReducer(t *testing.T) {
 			So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
 		})
 
-		Convey("diff", func() {
+		Convey("diff one point", func() {
+			result := testReducer("diff", 30)
+			So(result, ShouldEqual, float64(0))
+		})
+
+		Convey("diff two points", func() {
 			result := testReducer("diff", 30, 40)
 			So(result, ShouldEqual, float64(10))
 		})
 
-		Convey("percent_diff", func() {
+		Convey("diff three points", func() {
+			result := testReducer("diff", 30, 40, 40)
+			So(result, ShouldEqual, float64(10))
+		})
+
+		Convey("percent_diff one point", func() {
+			result := testReducer("percent_diff", 40)
+			So(result, ShouldEqual, float64(0))
+		})
+
+		Convey("percent_diff two points", func() {
 			result := testReducer("percent_diff", 30, 40)
 			So(result, ShouldEqual, float64(33.33333333333333))
 		})
 
+		Convey("percent_diff three points", func() {
+			result := testReducer("percent_diff", 30, 40, 40)
+			So(result, ShouldEqual, float64(33.33333333333333))
+		})
 	})
 }
 

+ 0 - 2
pkg/services/notifications/notifications.go

@@ -98,8 +98,6 @@ func (ns *NotificationService) Run(ctx context.Context) error {
 			return ctx.Err()
 		}
 	}
-
-	return nil
 }
 
 func (ns *NotificationService) SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {

+ 3 - 1
pkg/services/rendering/phantomjs.go

@@ -58,7 +58,9 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) (
 		cmdArgs = append([]string{fmt.Sprintf("--output-encoding=%s", opts.Encoding)}, cmdArgs...)
 	}
 
-	commandCtx, _ := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
+	commandCtx, cancel := context.WithTimeout(ctx, opts.Timeout+time.Second*2)
+	defer cancel()
+
 	cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...)
 	cmd.Stderr = cmd.Stdout
 

+ 1 - 1
pkg/tsdb/elasticsearch/client/client.go

@@ -218,7 +218,7 @@ func (c *baseClientImpl) ExecuteMultisearch(r *MultiSearchRequest) (*MultiSearch
 	elapsed := time.Now().Sub(start)
 	clientLog.Debug("Decoded multisearch json response", "took", elapsed)
 
-	msr.status = res.StatusCode
+	msr.Status = res.StatusCode
 
 	return &msr, nil
 }

+ 1 - 1
pkg/tsdb/elasticsearch/client/models.go

@@ -74,7 +74,7 @@ type MultiSearchRequest struct {
 
 // MultiSearchResponse represents a multi search response
 type MultiSearchResponse struct {
-	status    int               `json:"status,omitempty"`
+	Status    int               `json:"status,omitempty"`
 	Responses []*SearchResponse `json:"responses"`
 }
 

+ 64 - 34
public/app/containers/Explore/QueryField.tsx

@@ -9,7 +9,7 @@ import { getNextCharacter, getPreviousCousin } from './utils/dom';
 import BracesPlugin from './slate-plugins/braces';
 import ClearPlugin from './slate-plugins/clear';
 import NewlinePlugin from './slate-plugins/newline';
-import PluginPrism, { configurePrismMetricsTokens } from './slate-plugins/prism/index';
+import PluginPrism, { setPrismTokens } from './slate-plugins/prism/index';
 import RunnerPlugin from './slate-plugins/runner';
 import debounce from './utils/debounce';
 import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
@@ -17,13 +17,13 @@ import { processLabels, RATE_RANGES, cleanText } from './utils/prometheus';
 import Typeahead from './Typeahead';
 
 const EMPTY_METRIC = '';
-const TYPEAHEAD_DEBOUNCE = 300;
+export const TYPEAHEAD_DEBOUNCE = 300;
 
 function flattenSuggestions(s) {
   return s ? s.reduce((acc, g) => acc.concat(g.items), []) : [];
 }
 
-const getInitialValue = query =>
+export const getInitialValue = query =>
   Value.fromJSON({
     document: {
       nodes: [
@@ -45,12 +45,14 @@ const getInitialValue = query =>
     },
   });
 
-class Portal extends React.Component {
+class Portal extends React.Component<any, any> {
   node: any;
+
   constructor(props) {
     super(props);
+    const { index = 0, prefix = 'query' } = props;
     this.node = document.createElement('div');
-    this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`);
+    this.node.classList.add(`slate-typeahead`, `slate-typeahead-${prefix}-${index}`);
     document.body.appendChild(this.node);
   }
 
@@ -71,12 +73,14 @@ class QueryField extends React.Component<any, any> {
   constructor(props, context) {
     super(props, context);
 
+    const { prismDefinition = {}, prismLanguage = 'promql' } = props;
+
     this.plugins = [
       BracesPlugin(),
       ClearPlugin(),
       RunnerPlugin({ handler: props.onPressEnter }),
       NewlinePlugin(),
-      PluginPrism(),
+      PluginPrism({ definition: prismDefinition, language: prismLanguage }),
     ];
 
     this.state = {
@@ -131,7 +135,8 @@ class QueryField extends React.Component<any, any> {
     if (!this.state.metrics) {
       return;
     }
-    configurePrismMetricsTokens(this.state.metrics);
+    setPrismTokens(this.props.prismLanguage, 'metrics', this.state.metrics);
+
     // Trigger re-render
     window.requestAnimationFrame(() => {
       // Bogus edit to trigger highlighting
@@ -162,7 +167,7 @@ class QueryField extends React.Component<any, any> {
     const selection = window.getSelection();
     if (selection.anchorNode) {
       const wrapperNode = selection.anchorNode.parentElement;
-      const editorNode = wrapperNode.closest('.query-field');
+      const editorNode = wrapperNode.closest('.slate-query-field');
       if (!editorNode || this.state.value.isBlurred) {
         // Not inside this editor
         return;
@@ -330,20 +335,30 @@ class QueryField extends React.Component<any, any> {
   }
 
   onKeyDown = (event, change) => {
-    if (this.menuEl) {
-      const { typeaheadIndex, suggestions } = this.state;
-
-      switch (event.key) {
-        case 'Escape': {
-          if (this.menuEl) {
-            event.preventDefault();
-            this.resetTypeahead();
-            return true;
-          }
-          break;
+    const { typeaheadIndex, suggestions } = this.state;
+
+    switch (event.key) {
+      case 'Escape': {
+        if (this.menuEl) {
+          event.preventDefault();
+          event.stopPropagation();
+          this.resetTypeahead();
+          return true;
         }
+        break;
+      }
 
-        case 'Tab': {
+      case ' ': {
+        if (event.ctrlKey) {
+          event.preventDefault();
+          this.handleTypeahead();
+          return true;
+        }
+        break;
+      }
+
+      case 'Tab': {
+        if (this.menuEl) {
           // Dont blur input
           event.preventDefault();
           if (!suggestions || suggestions.length === 0) {
@@ -359,25 +374,30 @@ class QueryField extends React.Component<any, any> {
           this.applyTypeahead(change, suggestion);
           return true;
         }
+        break;
+      }
 
-        case 'ArrowDown': {
+      case 'ArrowDown': {
+        if (this.menuEl) {
           // Select next suggestion
           event.preventDefault();
           this.setState({ typeaheadIndex: typeaheadIndex + 1 });
-          break;
         }
+        break;
+      }
 
-        case 'ArrowUp': {
+      case 'ArrowUp': {
+        if (this.menuEl) {
           // Select previous suggestion
           event.preventDefault();
           this.setState({ typeaheadIndex: Math.max(0, typeaheadIndex - 1) });
-          break;
         }
+        break;
+      }
 
-        default: {
-          // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
-          break;
-        }
+      default: {
+        // console.log('default key', event.key, event.which, event.charCode, event.locale, data.key);
+        break;
       }
     }
     return undefined;
@@ -502,10 +522,17 @@ class QueryField extends React.Component<any, any> {
 
     // Align menu overlay to editor node
     if (node) {
+      // Read from DOM
       const rect = node.parentElement.getBoundingClientRect();
-      menu.style.opacity = 1;
-      menu.style.top = `${rect.top + window.scrollY + rect.height + 4}px`;
-      menu.style.left = `${rect.left + window.scrollX - 2}px`;
+      const scrollX = window.scrollX;
+      const scrollY = window.scrollY;
+
+      // Write DOM
+      requestAnimationFrame(() => {
+        menu.style.opacity = 1;
+        menu.style.top = `${rect.top + scrollY + rect.height + 4}px`;
+        menu.style.left = `${rect.left + scrollX - 2}px`;
+      });
     }
   };
 
@@ -514,6 +541,7 @@ class QueryField extends React.Component<any, any> {
   };
 
   renderMenu = () => {
+    const { portalPrefix } = this.props;
     const { suggestions } = this.state;
     const hasSuggesstions = suggestions && suggestions.length > 0;
     if (!hasSuggesstions) {
@@ -524,11 +552,13 @@ class QueryField extends React.Component<any, any> {
     let selectedIndex = Math.max(this.state.typeaheadIndex, 0);
     const flattenedSuggestions = flattenSuggestions(suggestions);
     selectedIndex = selectedIndex % flattenedSuggestions.length || 0;
-    const selectedKeys = flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : [];
+    const selectedKeys = (flattenedSuggestions.length > 0 ? [flattenedSuggestions[selectedIndex]] : []).map(
+      i => (typeof i === 'object' ? i.text : i)
+    );
 
     // Create typeahead in DOM root so we can later position it absolutely
     return (
-      <Portal>
+      <Portal prefix={portalPrefix}>
         <Typeahead
           menuRef={this.menuRef}
           selectedItems={selectedKeys}
@@ -541,7 +571,7 @@ class QueryField extends React.Component<any, any> {
 
   render() {
     return (
-      <div className="query-field">
+      <div className="slate-query-field">
         {this.renderMenu()}
         <Editor
           autoCorrect={false}

+ 5 - 1
public/app/containers/Explore/QueryRows.tsx

@@ -1,5 +1,6 @@
 import React, { PureComponent } from 'react';
 
+import promql from './slate-plugins/prism/promql';
 import QueryField from './QueryField';
 
 class QueryRow extends PureComponent<any, any> {
@@ -55,12 +56,15 @@ class QueryRow extends PureComponent<any, any> {
             <i className="fa fa-minus" />
           </button>
         </div>
-        <div className="query-field-wrapper">
+        <div className="slate-query-field-wrapper">
           <QueryField
             initialQuery={edited ? null : query}
+            portalPrefix="explore"
             onPressEnter={this.handlePressEnter}
             onQueryChange={this.handleChangeQuery}
             placeholder="Enter a PromQL query"
+            prismLanguage="promql"
+            prismDefinition={promql}
             request={request}
           />
         </div>

+ 15 - 4
public/app/containers/Explore/Typeahead.tsx

@@ -23,12 +23,13 @@ class TypeaheadItem extends React.PureComponent<any, any> {
   };
 
   render() {
-    const { isSelected, label, onClickItem } = this.props;
+    const { hint, isSelected, label, onClickItem } = this.props;
     const className = isSelected ? 'typeahead-item typeahead-item__selected' : 'typeahead-item';
     const onClick = () => onClickItem(label);
     return (
       <li ref={this.getRef} className={className} onClick={onClick}>
         {label}
+        {hint && isSelected ? <div className="typeahead-item-hint">{hint}</div> : null}
       </li>
     );
   }
@@ -41,9 +42,19 @@ class TypeaheadGroup extends React.PureComponent<any, any> {
       <li className="typeahead-group">
         <div className="typeahead-group__title">{label}</div>
         <ul className="typeahead-group__list">
-          {items.map(item => (
-            <TypeaheadItem key={item} onClickItem={onClickItem} isSelected={selected.indexOf(item) > -1} label={item} />
-          ))}
+          {items.map(item => {
+            const text = typeof item === 'object' ? item.text : item;
+            const label = typeof item === 'object' ? item.display || item.text : item;
+            return (
+              <TypeaheadItem
+                key={text}
+                onClickItem={onClickItem}
+                isSelected={selected.indexOf(text) > -1}
+                hint={item.hint}
+                label={label}
+              />
+            );
+          })}
         </ul>
       </li>
     );

+ 11 - 10
public/app/containers/Explore/slate-plugins/prism/index.tsx

@@ -1,16 +1,12 @@
 import React from 'react';
 import Prism from 'prismjs';
 
-import Promql from './promql';
-
-Prism.languages.promql = Promql;
-
 const TOKEN_MARK = 'prism-token';
 
-export function configurePrismMetricsTokens(metrics) {
-  Prism.languages.promql.metric = {
-    alias: 'variable',
-    pattern: new RegExp(`(?:^|\\s)(${metrics.join('|')})(?:$|\\s)`),
+export function setPrismTokens(language, field, values, alias = 'variable') {
+  Prism.languages[language][field] = {
+    alias,
+    pattern: new RegExp(`(?:^|\\s)(${values.join('|')})(?:$|\\s)`),
   };
 }
 
@@ -21,7 +17,12 @@ export function configurePrismMetricsTokens(metrics) {
  * (Adapted to handle nested grammar definitions.)
  */
 
-export default function PrismPlugin() {
+export default function PrismPlugin({ definition, language }) {
+  if (definition) {
+    // Don't override exising modified definitions
+    Prism.languages[language] = Prism.languages[language] || definition;
+  }
+
   return {
     /**
      * Render a Slate mark with appropiate CSS class names
@@ -54,7 +55,7 @@ export default function PrismPlugin() {
 
       const texts = node.getTexts().toArray();
       const tstring = texts.map(t => t.text).join('\n');
-      const grammar = Prism.languages.promql;
+      const grammar = Prism.languages[language];
       const tokens = Prism.tokenize(tstring, grammar);
       const decorations = [];
       let startText = texts.shift();

+ 1 - 0
public/app/plugins/datasource/cloudwatch/datasource.ts

@@ -404,6 +404,7 @@ export default class CloudWatchDatasource {
   }
 
   expandTemplateVariable(targets, scopedVars, templateSrv) {
+    // Datasource and template srv logic uber-complected. This should be cleaned up.
     return _.chain(targets)
       .map(target => {
         var dimensionKey = _.findKey(target.dimensions, v => {

+ 75 - 172
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts → public/app/plugins/datasource/cloudwatch/specs/datasource.jest.ts

@@ -1,32 +1,38 @@
 import '../datasource';
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
-import helpers from 'test/specs/helpers';
 import CloudWatchDatasource from '../datasource';
-import 'app/features/dashboard/time_srv';
+import * as dateMath from 'app/core/utils/datemath';
+import _ from 'lodash';
 
 describe('CloudWatchDatasource', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = {
+  let instanceSettings = {
     jsonData: { defaultRegion: 'us-east-1', access: 'proxy' },
   };
 
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(angularMocks.module('grafana.controllers'));
-  beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
-  beforeEach(ctx.createService('timeSrv'));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(CloudWatchDatasource, {
-        instanceSettings: instanceSettings,
-      });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
+  let templateSrv = {
+    data: {},
+    templateSettings: { interpolate: /\[\[([\s\S]+?)\]\]/g },
+    replace: text => _.template(text, templateSrv.templateSettings)(templateSrv.data),
+    variableExists: () => false,
+  };
+
+  let timeSrv = {
+    time: { from: 'now-1h', to: 'now' },
+    timeRange: () => {
+      return {
+        from: dateMath.parse(timeSrv.time.from, false),
+        to: dateMath.parse(timeSrv.time.to, true),
+      };
+    },
+  };
+  let backendSrv = {};
+  let ctx = <any>{
+    backendSrv,
+    templateSrv,
+  };
+
+  beforeEach(() => {
+    ctx.ds = new CloudWatchDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv);
+  });
 
   describe('When performing CloudWatch query', function() {
     var requestParams;
@@ -67,24 +73,23 @@ describe('CloudWatchDatasource', function() {
       },
     };
 
-    beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(params) {
+    beforeEach(() => {
+      ctx.backendSrv.datasourceRequest = jest.fn(params => {
         requestParams = params.data;
-        return ctx.$q.when({ data: response });
-      };
+        return Promise.resolve({ data: response });
+      });
     });
 
     it('should generate the correct query', function(done) {
       ctx.ds.query(query).then(function() {
         var params = requestParams.queries[0];
-        expect(params.namespace).to.be(query.targets[0].namespace);
-        expect(params.metricName).to.be(query.targets[0].metricName);
-        expect(params.dimensions['InstanceId']).to.be('i-12345678');
-        expect(params.statistics).to.eql(query.targets[0].statistics);
-        expect(params.period).to.be(query.targets[0].period);
+        expect(params.namespace).toBe(query.targets[0].namespace);
+        expect(params.metricName).toBe(query.targets[0].metricName);
+        expect(params.dimensions['InstanceId']).toBe('i-12345678');
+        expect(params.statistics).toEqual(query.targets[0].statistics);
+        expect(params.period).toBe(query.targets[0].period);
         done();
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should generate the correct query with interval variable', function(done) {
@@ -111,116 +116,17 @@ describe('CloudWatchDatasource', function() {
 
       ctx.ds.query(query).then(function() {
         var params = requestParams.queries[0];
-        expect(params.period).to.be('600');
+        expect(params.period).toBe('600');
         done();
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return series list', function(done) {
       ctx.ds.query(query).then(function(result) {
-        expect(result.data[0].target).to.be(response.results.A.series[0].name);
-        expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
+        expect(result.data[0].target).toBe(response.results.A.series[0].name);
+        expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]);
         done();
       });
-      ctx.$rootScope.$apply();
-    });
-
-    it('should generate the correct targets by expanding template variables', function() {
-      var templateSrv = {
-        variables: [
-          {
-            name: 'instance_id',
-            options: [
-              { text: 'i-23456789', value: 'i-23456789', selected: false },
-              { text: 'i-34567890', value: 'i-34567890', selected: true },
-            ],
-            current: {
-              text: 'i-34567890',
-              value: 'i-34567890',
-            },
-          },
-        ],
-        replace: function(target, scopedVars) {
-          if (target === '$instance_id' && scopedVars['instance_id']['text'] === 'i-34567890') {
-            return 'i-34567890';
-          } else {
-            return '';
-          }
-        },
-        getVariableName: function(e) {
-          return 'instance_id';
-        },
-        variableExists: function(e) {
-          return true;
-        },
-        containsVariable: function(str, variableName) {
-          return str.indexOf('$' + variableName) !== -1;
-        },
-      };
-
-      var targets = [
-        {
-          region: 'us-east-1',
-          namespace: 'AWS/EC2',
-          metricName: 'CPUUtilization',
-          dimensions: {
-            InstanceId: '$instance_id',
-          },
-          statistics: ['Average'],
-          period: 300,
-        },
-      ];
-
-      var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv);
-      expect(result[0].dimensions.InstanceId).to.be('i-34567890');
-    });
-
-    it('should generate the correct targets by expanding template variables from url', function() {
-      var templateSrv = {
-        variables: [
-          {
-            name: 'instance_id',
-            options: [
-              { text: 'i-23456789', value: 'i-23456789', selected: false },
-              { text: 'i-34567890', value: 'i-34567890', selected: false },
-            ],
-            current: 'i-45678901',
-          },
-        ],
-        replace: function(target, scopedVars) {
-          if (target === '$instance_id') {
-            return 'i-45678901';
-          } else {
-            return '';
-          }
-        },
-        getVariableName: function(e) {
-          return 'instance_id';
-        },
-        variableExists: function(e) {
-          return true;
-        },
-        containsVariable: function(str, variableName) {
-          return str.indexOf('$' + variableName) !== -1;
-        },
-      };
-
-      var targets = [
-        {
-          region: 'us-east-1',
-          namespace: 'AWS/EC2',
-          metricName: 'CPUUtilization',
-          dimensions: {
-            InstanceId: '$instance_id',
-          },
-          statistics: ['Average'],
-          period: 300,
-        },
-      ];
-
-      var result = ctx.ds.expandTemplateVariable(targets, {}, templateSrv);
-      expect(result[0].dimensions.InstanceId).to.be('i-45678901');
     });
   });
 
@@ -228,21 +134,21 @@ describe('CloudWatchDatasource', function() {
     it('should return the datasource region if empty or "default"', function() {
       var defaultRegion = instanceSettings.jsonData.defaultRegion;
 
-      expect(ctx.ds.getActualRegion()).to.be(defaultRegion);
-      expect(ctx.ds.getActualRegion('')).to.be(defaultRegion);
-      expect(ctx.ds.getActualRegion('default')).to.be(defaultRegion);
+      expect(ctx.ds.getActualRegion()).toBe(defaultRegion);
+      expect(ctx.ds.getActualRegion('')).toBe(defaultRegion);
+      expect(ctx.ds.getActualRegion('default')).toBe(defaultRegion);
     });
 
     it('should return the specified region if specified', function() {
-      expect(ctx.ds.getActualRegion('some-fake-region-1')).to.be('some-fake-region-1');
+      expect(ctx.ds.getActualRegion('some-fake-region-1')).toBe('some-fake-region-1');
     });
 
     var requestParams;
     beforeEach(function() {
-      ctx.ds.performTimeSeriesQuery = function(request) {
+      ctx.ds.performTimeSeriesQuery = jest.fn(request => {
         requestParams = request;
-        return ctx.$q.when({ data: {} });
-      };
+        return Promise.resolve({ data: {} });
+      });
     });
 
     it('should query for the datasource region if empty or "default"', function(done) {
@@ -264,10 +170,9 @@ describe('CloudWatchDatasource', function() {
       };
 
       ctx.ds.query(query).then(function(result) {
-        expect(requestParams.queries[0].region).to.be(instanceSettings.jsonData.defaultRegion);
+        expect(requestParams.queries[0].region).toBe(instanceSettings.jsonData.defaultRegion);
         done();
       });
-      ctx.$rootScope.$apply();
     });
   });
 
@@ -311,18 +216,17 @@ describe('CloudWatchDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(params) {
-        return ctx.$q.when({ data: response });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(params => {
+        return Promise.resolve({ data: response });
+      });
     });
 
     it('should return series list', function(done) {
       ctx.ds.query(query).then(function(result) {
-        expect(result.data[0].target).to.be(response.results.A.series[0].name);
-        expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
+        expect(result.data[0].target).toBe(response.results.A.series[0].name);
+        expect(result.data[0].datapoints[0][0]).toBe(response.results.A.series[0].points[0][0]);
         done();
       });
-      ctx.$rootScope.$apply();
     });
   });
 
@@ -332,14 +236,13 @@ describe('CloudWatchDatasource', function() {
       scenario.setup = setupCallback => {
         beforeEach(() => {
           setupCallback();
-          ctx.backendSrv.datasourceRequest = args => {
+          ctx.backendSrv.datasourceRequest = jest.fn(args => {
             scenario.request = args.data;
-            return ctx.$q.when({ data: scenario.requestResponse });
-          };
+            return Promise.resolve({ data: scenario.requestResponse });
+          });
           ctx.ds.metricFindQuery(query).then(args => {
             scenario.result = args;
           });
-          ctx.$rootScope.$apply();
         });
       };
 
@@ -359,9 +262,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __GetRegions and return result', () => {
-      expect(scenario.result[0].text).to.contain('us-east-1');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('regions');
+      expect(scenario.result[0].text).toContain('us-east-1');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('regions');
     });
   });
 
@@ -377,9 +280,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __GetNamespaces and return result', () => {
-      expect(scenario.result[0].text).to.contain('AWS/EC2');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('namespaces');
+      expect(scenario.result[0].text).toContain('AWS/EC2');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('namespaces');
     });
   });
 
@@ -395,9 +298,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __GetMetrics and return result', () => {
-      expect(scenario.result[0].text).to.be('CPUUtilization');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('metrics');
+      expect(scenario.result[0].text).toBe('CPUUtilization');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('metrics');
     });
   });
 
@@ -413,9 +316,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __GetDimensions and return result', () => {
-      expect(scenario.result[0].text).to.be('InstanceId');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('dimension_keys');
+      expect(scenario.result[0].text).toBe('InstanceId');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('dimension_keys');
     });
   });
 
@@ -431,9 +334,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __ListMetrics and return result', () => {
-      expect(scenario.result[0].text).to.contain('i-12345678');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('dimension_values');
+      expect(scenario.result[0].text).toContain('i-12345678');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('dimension_values');
     });
   });
 
@@ -449,9 +352,9 @@ describe('CloudWatchDatasource', function() {
     });
 
     it('should call __ListMetrics and return result', () => {
-      expect(scenario.result[0].text).to.contain('i-12345678');
-      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
-      expect(scenario.request.queries[0].subtype).to.be('dimension_values');
+      expect(scenario.result[0].text).toContain('i-12345678');
+      expect(scenario.request.queries[0].type).toBe('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).toBe('dimension_values');
     });
   });
 
@@ -544,7 +447,7 @@ describe('CloudWatchDatasource', function() {
       let now = new Date(options.range.from.valueOf() + t[2] * 1000);
       let expected = t[3];
       let actual = ctx.ds.getPeriod(target, options, now);
-      expect(actual).to.be(expected);
+      expect(actual).toBe(expected);
     }
   });
 });

+ 48 - 59
public/app/plugins/datasource/mysql/specs/datasource_specs.ts → public/app/plugins/datasource/mysql/specs/datasource.jest.ts

@@ -1,28 +1,21 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
 import moment from 'moment';
-import helpers from 'test/specs/helpers';
 import { MysqlDatasource } from '../datasource';
 import { CustomVariable } from 'app/features/templating/custom_variable';
 
 describe('MySQLDatasource', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = { name: 'mysql' };
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase(['backendSrv']));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(MysqlDatasource, {
-        instanceSettings: instanceSettings,
-      });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
+  let instanceSettings = { name: 'mysql' };
+  let backendSrv = {};
+  let templateSrv = {
+    replace: jest.fn(text => text),
+  };
+
+  let ctx = <any>{
+    backendSrv,
+  };
+
+  beforeEach(() => {
+    ctx.ds = new MysqlDatasource(instanceSettings, backendSrv, {}, templateSrv);
+  });
 
   describe('When performing annotationQuery', function() {
     let results;
@@ -59,26 +52,25 @@ describe('MySQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.annotationQuery(options).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return annotation list', function() {
-      expect(results.length).to.be(3);
+      expect(results.length).toBe(3);
 
-      expect(results[0].text).to.be('some text');
-      expect(results[0].tags[0]).to.be('TagA');
-      expect(results[0].tags[1]).to.be('TagB');
+      expect(results[0].text).toBe('some text');
+      expect(results[0].tags[0]).toBe('TagA');
+      expect(results[0].tags[1]).toBe('TagB');
 
-      expect(results[1].tags[0]).to.be('TagB');
-      expect(results[1].tags[1]).to.be('TagC');
+      expect(results[1].tags[0]).toBe('TagB');
+      expect(results[1].tags[1]).toBe('TagC');
 
-      expect(results[2].tags.length).to.be(0);
+      expect(results[2].tags.length).toBe(0);
     });
   });
 
@@ -103,19 +95,18 @@ describe('MySQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return list of all column values', function() {
-      expect(results.length).to.be(6);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[5].text).to.be('some text3');
+      expect(results.length).toBe(6);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[5].text).toBe('some text3');
     });
   });
 
@@ -140,21 +131,20 @@ describe('MySQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return list of as text, value', function() {
-      expect(results.length).to.be(3);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[0].value).to.be('value1');
-      expect(results[2].text).to.be('aTitle3');
-      expect(results[2].value).to.be('value3');
+      expect(results.length).toBe(3);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[0].value).toBe('value1');
+      expect(results[2].text).toBe('aTitle3');
+      expect(results[2].value).toBe('value3');
     });
   });
 
@@ -179,19 +169,18 @@ describe('MySQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return list of unique keys', function() {
-      expect(results.length).to.be(1);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[0].value).to.be('same');
+      expect(results.length).toBe(1);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[0].value).toBe('same');
     });
   });
 
@@ -202,33 +191,33 @@ describe('MySQLDatasource', function() {
 
     describe('and value is a string', () => {
       it('should return an unquoted value', () => {
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc');
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc');
       });
     });
 
     describe('and value is a number', () => {
       it('should return an unquoted value', () => {
-        expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000);
+        expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000);
       });
     });
 
     describe('and value is an array of strings', () => {
       it('should return comma separated quoted values', () => {
-        expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'");
+        expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'");
       });
     });
 
     describe('and variable allows multi-value and value is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.multi = true;
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
       });
     });
 
     describe('and variable allows all and value is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.includeAll = true;
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
       });
     });
   });

+ 50 - 60
public/app/plugins/datasource/postgres/specs/datasource_specs.ts → public/app/plugins/datasource/postgres/specs/datasource.jest.ts

@@ -1,28 +1,21 @@
-import { describe, beforeEach, it, expect, angularMocks } from 'test/lib/common';
 import moment from 'moment';
-import helpers from 'test/specs/helpers';
 import { PostgresDatasource } from '../datasource';
 import { CustomVariable } from 'app/features/templating/custom_variable';
 
 describe('PostgreSQLDatasource', function() {
-  var ctx = new helpers.ServiceTestContext();
-  var instanceSettings = { name: 'postgresql' };
-
-  beforeEach(angularMocks.module('grafana.core'));
-  beforeEach(angularMocks.module('grafana.services'));
-  beforeEach(ctx.providePhase(['backendSrv']));
-
-  beforeEach(
-    angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
-      ctx.$q = $q;
-      ctx.$httpBackend = $httpBackend;
-      ctx.$rootScope = $rootScope;
-      ctx.ds = $injector.instantiate(PostgresDatasource, {
-        instanceSettings: instanceSettings,
-      });
-      $httpBackend.when('GET', /\.html$/).respond('');
-    })
-  );
+  let instanceSettings = { name: 'postgresql' };
+
+  let backendSrv = {};
+  let templateSrv = {
+    replace: jest.fn(text => text),
+  };
+  let ctx = <any>{
+    backendSrv,
+  };
+
+  beforeEach(() => {
+    ctx.ds = new PostgresDatasource(instanceSettings, backendSrv, {}, templateSrv);
+  });
 
   describe('When performing annotationQuery', function() {
     let results;
@@ -59,26 +52,25 @@ describe('PostgreSQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.annotationQuery(options).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return annotation list', function() {
-      expect(results.length).to.be(3);
+      expect(results.length).toBe(3);
 
-      expect(results[0].text).to.be('some text');
-      expect(results[0].tags[0]).to.be('TagA');
-      expect(results[0].tags[1]).to.be('TagB');
+      expect(results[0].text).toBe('some text');
+      expect(results[0].tags[0]).toBe('TagA');
+      expect(results[0].tags[1]).toBe('TagB');
 
-      expect(results[1].tags[0]).to.be('TagB');
-      expect(results[1].tags[1]).to.be('TagC');
+      expect(results[1].tags[0]).toBe('TagB');
+      expect(results[1].tags[1]).toBe('TagC');
 
-      expect(results[2].tags.length).to.be(0);
+      expect(results[2].tags.length).toBe(0);
     });
   });
 
@@ -103,19 +95,18 @@ describe('PostgreSQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return list of all column values', function() {
-      expect(results.length).to.be(6);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[5].text).to.be('some text3');
+      expect(results.length).toBe(6);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[5].text).toBe('some text3');
     });
   });
 
@@ -140,21 +131,20 @@ describe('PostgreSQLDatasource', function() {
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
     });
 
     it('should return list of as text, value', function() {
-      expect(results.length).to.be(3);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[0].value).to.be('value1');
-      expect(results[2].text).to.be('aTitle3');
-      expect(results[2].value).to.be('value3');
+      expect(results.length).toBe(3);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[0].value).toBe('value1');
+      expect(results[2].text).toBe('aTitle3');
+      expect(results[2].value).toBe('value3');
     });
   });
 
@@ -178,20 +168,20 @@ describe('PostgreSQLDatasource', function() {
       },
     };
 
-    beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(options) {
-        return ctx.$q.when({ data: response, status: 200 });
-      };
+    beforeEach(() => {
+      ctx.backendSrv.datasourceRequest = jest.fn(options => {
+        return Promise.resolve({ data: response, status: 200 });
+      });
       ctx.ds.metricFindQuery(query).then(function(data) {
         results = data;
       });
-      ctx.$rootScope.$apply();
+      //ctx.$rootScope.$apply();
     });
 
     it('should return list of unique keys', function() {
-      expect(results.length).to.be(1);
-      expect(results[0].text).to.be('aTitle');
-      expect(results[0].value).to.be('same');
+      expect(results.length).toBe(1);
+      expect(results[0].text).toBe('aTitle');
+      expect(results[0].value).toBe('same');
     });
   });
 
@@ -202,33 +192,33 @@ describe('PostgreSQLDatasource', function() {
 
     describe('and value is a string', () => {
       it('should return an unquoted value', () => {
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql('abc');
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual('abc');
       });
     });
 
     describe('and value is a number', () => {
       it('should return an unquoted value', () => {
-        expect(ctx.ds.interpolateVariable(1000, ctx.variable)).to.eql(1000);
+        expect(ctx.ds.interpolateVariable(1000, ctx.variable)).toEqual(1000);
       });
     });
 
     describe('and value is an array of strings', () => {
       it('should return comma separated quoted values', () => {
-        expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).to.eql("'a','b','c'");
+        expect(ctx.ds.interpolateVariable(['a', 'b', 'c'], ctx.variable)).toEqual("'a','b','c'");
       });
     });
 
     describe('and variable allows multi-value and is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.multi = true;
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
       });
     });
 
     describe('and variable allows all and is a string', () => {
       it('should return a quoted value', () => {
         ctx.variable.includeAll = true;
-        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).to.eql("'abc'");
+        expect(ctx.ds.interpolateVariable('abc', ctx.variable)).toEqual("'abc'");
       });
     });
   });

+ 1 - 1
public/app/plugins/panel/heatmap/color_scale.ts

@@ -3,7 +3,7 @@ import * as d3ScaleChromatic from 'd3-scale-chromatic';
 
 export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any {
   let colorInterpolator = d3ScaleChromatic[colorScheme.value];
-  let colorScaleInverted = colorScheme.invert === 'always' || (colorScheme.invert === 'dark' && !lightTheme);
+  let colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark');
 
   let start = colorScaleInverted ? maxValue : minValue;
   let end = colorScaleInverted ? minValue : maxValue;

+ 8 - 1
public/app/plugins/panel/heatmap/heatmap_ctrl.ts

@@ -76,6 +76,13 @@ let colorSchemes = [
   { name: 'Reds', value: 'interpolateReds', invert: 'dark' },
 
   // Sequential (Multi-Hue)
+  { name: 'Viridis', value: 'interpolateViridis', invert: 'light' },
+  { name: 'Magma', value: 'interpolateMagma', invert: 'light' },
+  { name: 'Inferno', value: 'interpolateInferno', invert: 'light' },
+  { name: 'Plasma', value: 'interpolatePlasma', invert: 'light' },
+  { name: 'Warm', value: 'interpolateWarm', invert: 'light' },
+  { name: 'Cool', value: 'interpolateCool', invert: 'light' },
+  { name: 'Cubehelix', value: 'interpolateCubehelixDefault', invert: 'light' },
   { name: 'BuGn', value: 'interpolateBuGn', invert: 'dark' },
   { name: 'BuPu', value: 'interpolateBuPu', invert: 'dark' },
   { name: 'GnBu', value: 'interpolateGnBu', invert: 'dark' },
@@ -87,7 +94,7 @@ let colorSchemes = [
   { name: 'YlGnBu', value: 'interpolateYlGnBu', invert: 'dark' },
   { name: 'YlGn', value: 'interpolateYlGn', invert: 'dark' },
   { name: 'YlOrBr', value: 'interpolateYlOrBr', invert: 'dark' },
-  { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'darm' },
+  { name: 'YlOrRd', value: 'interpolateYlOrRd', invert: 'dark' },
 ];
 
 const ds_support_histogram_sort = ['prometheus', 'elasticsearch'];

+ 1 - 0
public/sass/_grafana.scss

@@ -67,6 +67,7 @@
 @import 'components/filter-list';
 @import 'components/filter-table';
 @import 'components/old_stuff';
+@import 'components/slate_editor';
 @import 'components/typeahead';
 @import 'components/modals';
 @import 'components/dropdown';

+ 151 - 0
public/sass/components/_slate_editor.scss

@@ -0,0 +1,151 @@
+.slate-query-field {
+  font-size: $font-size-root;
+  font-family: $font-family-monospace;
+  height: auto;
+}
+
+.slate-query-field-wrapper {
+  position: relative;
+  display: inline-block;
+  padding: 6px 7px 4px;
+  width: 100%;
+  cursor: text;
+  line-height: $line-height-base;
+  color: $text-color-weak;
+  background-color: $panel-bg;
+  background-image: none;
+  border: $panel-border;
+  border-radius: $border-radius;
+  transition: all 0.3s;
+}
+
+.slate-typeahead {
+  .typeahead {
+    position: absolute;
+    z-index: auto;
+    top: -10000px;
+    left: -10000px;
+    opacity: 0;
+    border-radius: $border-radius;
+    transition: opacity 0.75s;
+    border: $panel-border;
+    max-height: calc(66vh);
+    overflow-y: scroll;
+    max-width: calc(66%);
+    overflow-x: hidden;
+    outline: none;
+    list-style: none;
+    background: $panel-bg;
+    color: $text-color;
+    transition: opacity 0.4s ease-out;
+    box-shadow: $typeahead-shadow;
+  }
+
+  .typeahead-group__title {
+    color: $text-color-weak;
+    font-size: $font-size-sm;
+    line-height: $line-height-base;
+    padding: $input-padding-y $input-padding-x;
+  }
+
+  .typeahead-item {
+    height: auto;
+    font-family: $font-family-monospace;
+    padding: $input-padding-y $input-padding-x;
+    padding-left: $input-padding-x-lg;
+    font-size: $font-size-sm;
+    text-overflow: ellipsis;
+    overflow: hidden;
+    z-index: 1;
+    display: block;
+    white-space: nowrap;
+    cursor: pointer;
+    transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
+      background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1);
+  }
+
+  .typeahead-item__selected {
+    background-color: $typeahead-selected-bg;
+    color: $typeahead-selected-color;
+
+    .typeahead-item-hint {
+      font-size: $font-size-xs;
+      color: $text-color;
+    }
+  }
+}
+
+/* SYNTAX */
+
+.slate-query-field {
+  .token.comment,
+  .token.block-comment,
+  .token.prolog,
+  .token.doctype,
+  .token.cdata {
+    color: $text-color-weak;
+  }
+
+  .token.punctuation {
+    color: $text-color-weak;
+  }
+
+  .token.property,
+  .token.tag,
+  .token.boolean,
+  .token.number,
+  .token.function-name,
+  .token.constant,
+  .token.symbol,
+  .token.deleted {
+    color: $query-red;
+  }
+
+  .token.selector,
+  .token.attr-name,
+  .token.string,
+  .token.char,
+  .token.function,
+  .token.builtin,
+  .token.inserted {
+    color: $query-green;
+  }
+
+  .token.operator,
+  .token.entity,
+  .token.url,
+  .token.variable {
+    color: $query-purple;
+  }
+
+  .token.atrule,
+  .token.attr-value,
+  .token.keyword,
+  .token.class-name {
+    color: $query-blue;
+  }
+
+  .token.regex,
+  .token.important {
+    color: $query-orange;
+  }
+
+  .token.important {
+    font-weight: normal;
+  }
+
+  .token.bold {
+    font-weight: bold;
+  }
+  .token.italic {
+    font-style: italic;
+  }
+
+  .token.entity {
+    cursor: help;
+  }
+
+  .namespace {
+    opacity: 0.7;
+  }
+}

+ 0 - 147
public/sass/pages/_explore.scss

@@ -93,150 +93,3 @@
 .query-row-tools {
   width: 4rem;
 }
-
-.query-field {
-  font-size: $font-size-root;
-  font-family: $font-family-monospace;
-  height: auto;
-}
-
-.query-field-wrapper {
-  position: relative;
-  display: inline-block;
-  padding: 6px 7px 4px;
-  width: 100%;
-  cursor: text;
-  line-height: $line-height-base;
-  color: $text-color-weak;
-  background-color: $panel-bg;
-  background-image: none;
-  border: $panel-border;
-  border-radius: $border-radius;
-  transition: all 0.3s;
-}
-
-.explore-typeahead {
-  .typeahead {
-    position: absolute;
-    z-index: auto;
-    top: -10000px;
-    left: -10000px;
-    opacity: 0;
-    border-radius: $border-radius;
-    transition: opacity 0.75s;
-    border: $panel-border;
-    max-height: calc(66vh);
-    overflow-y: scroll;
-    max-width: calc(66%);
-    overflow-x: hidden;
-    outline: none;
-    list-style: none;
-    background: $panel-bg;
-    color: $text-color;
-    transition: opacity 0.4s ease-out;
-    box-shadow: $typeahead-shadow;
-  }
-
-  .typeahead-group__title {
-    color: $text-color-weak;
-    font-size: $font-size-sm;
-    line-height: $line-height-base;
-    padding: $input-padding-y $input-padding-x;
-  }
-
-  .typeahead-item {
-    height: auto;
-    font-family: $font-family-monospace;
-    padding: $input-padding-y $input-padding-x;
-    padding-left: $input-padding-x-lg;
-    font-size: $font-size-sm;
-    text-overflow: ellipsis;
-    overflow: hidden;
-    z-index: 1;
-    display: block;
-    white-space: nowrap;
-    cursor: pointer;
-    transition: color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), border-color 0.3s cubic-bezier(0.645, 0.045, 0.355, 1),
-      background 0.3s cubic-bezier(0.645, 0.045, 0.355, 1), padding 0.15s cubic-bezier(0.645, 0.045, 0.355, 1);
-  }
-
-  .typeahead-item__selected {
-    background-color: $typeahead-selected-bg;
-    color: $typeahead-selected-color;
-  }
-}
-
-/* SYNTAX */
-
-.explore {
-  .token.comment,
-  .token.block-comment,
-  .token.prolog,
-  .token.doctype,
-  .token.cdata {
-    color: $text-color-weak;
-  }
-
-  .token.punctuation {
-    color: $text-color-weak;
-  }
-
-  .token.property,
-  .token.tag,
-  .token.boolean,
-  .token.number,
-  .token.function-name,
-  .token.constant,
-  .token.symbol,
-  .token.deleted {
-    color: $query-red;
-  }
-
-  .token.selector,
-  .token.attr-name,
-  .token.string,
-  .token.char,
-  .token.function,
-  .token.builtin,
-  .token.inserted {
-    color: $query-green;
-  }
-
-  .token.operator,
-  .token.entity,
-  .token.url,
-  .token.variable {
-    color: $query-purple;
-  }
-
-  .token.atrule,
-  .token.attr-value,
-  .token.keyword,
-  .token.class-name {
-    color: $query-blue;
-  }
-
-  .token.regex,
-  .token.important {
-    color: $query-orange;
-  }
-
-  .token.important {
-    font-weight: normal;
-  }
-
-  .token.bold {
-    font-weight: bold;
-  }
-  .token.italic {
-    font-style: italic;
-  }
-
-  .token.entity {
-    cursor: help;
-  }
-
-  .namespace {
-    opacity: 0.7;
-  }
-}

+ 0 - 3
scripts/circle-test-backend.sh

@@ -13,9 +13,6 @@ function exit_if_fail {
 echo "running go fmt"
 exit_if_fail test -z "$(gofmt -s -l ./pkg | tee /dev/stderr)"
 
-echo "running go vet"
-exit_if_fail test -z "$(go vet ./pkg/... | tee /dev/stderr)"
-
 echo "building backend with install to cache pkgs"
 exit_if_fail time go install ./pkg/cmd/grafana-server
 

+ 1 - 1
tslint.json

@@ -2,7 +2,7 @@
   "rules": {
     "no-string-throw": true,
     "no-unused-expression": true,
-		"no-unused-variable": false,
+    "no-unused-variable": false,
     "no-use-before-declare": false,
     "no-duplicate-variable": true,
     "curly": true,